全局唯一ID要求
分布式系统中,我们会对一些数据量大的业务进行拆分,如用户表、订单表,当数据量巨大导致数据库性能下降时,通常会进行分库分表,无法利用MySQL的自增ID,那么就需要一个单独的系统来生成全局唯一ID,而且生成的ID要求具有以下特性:
- 整个系统全局唯一
- ID趋势递增,提高数据库插入的效率(索引是递增的,避免乱序插入提高索引的维护成本)
- ID简单,占用空间小,查询效率高
常见方案
UUID
全局唯一首先可以想到使用UUID,基本各种语言都提供了UUID的库
优点:
- 代码实现简单
- 本地生成,没有性能问题
- 全球唯一的,数据迁移容易
缺点:
- 每次生成的ID是无序的,不满足趋势递增
- UUID是字符串,而且比较长,占用空间大,查询效率低
- ID没有含义,可读性差
MySQL自增主键
单表可以使用MySQL的自增ID,多表的情况下其实也可以使用自增ID,只是和单表每次+1不同,分多表的情况下每次需要加N,具体如下图:
上图中共分成了两个库4个表,那么每个表初始值一次为1~4,之后每次自增时+4,这样保证了每个表的ID不会重复,而且是趋势递增的,解决了单表的问题。
缺点:
一旦步长定好就无法扩容,数据库单机能力有限,不易于横向扩展
雪花snowflake方案
雪花算法生成64位二进制正整数,然后转换为10进制的数,具体方案如下图:
最高的一个bit不用,接下来的41个bit表示微秒级的时间(表示范围约69年),再接下来的10个bit机器编号可以分别表示1024台机器,如果对IDC划分,可以将10-bit的高几位表示IDC,最低位的12个字节是一个自增序列,表示范围为2^12=4096个,理论上这种方案每秒可以生成的唯一ID数约为4096*1000=409.6w个。
优点:
- 整个ID满足趋势递增
- 不依赖第三方系统,稳定性和性能都比较高
- 可以根据自身业务分配bit位,比较灵活
缺点:
- 强依赖系统时钟,如果系统时钟回拨,会导致ID重复或者服务不可用
Redis
利用Redis的incr原子性操作
一般方案为年份+月份+小时+Redis自增。
优点:
- 有序递增,可读性强
- 性能较高
缺点:
- 占用带宽,依赖Redis
更优的方案
美团的Leaf-segment方案
在之前的数据方案中,利用自增id每次从数据库只取了一个id,由于数据库的IO能力有限,不能支持高并发的场景,那么如果一次取一批id,消耗完再取一批,是不是就可以提高并发能力了?具体的方案架构如下:
抽象出Proxy Server,用于从数据库批量获取,然后在Leaf内部中逐个消耗分发给用户服务。
如图中的数据库表结构:biz_tag用来区分业务,业务之间的id号相互隔离互不影响,每次从数据库取出step个id号,将数据操作次数减少到了1/step。
对于多个Leaf抢占数据库可以利用MySQL的事务和锁机制,先更新再查询,保证多个Leaf请求的id范围不会重复复。
Begin
UPDATE table SET max_id=max_id+step WHERE biz_tag=xxx
SELECT tag, max_id, step FROM table WHERE biz_tag=xxx
Commit
这样返回给业务服务的ID范围应该是[max_id-step+1, max_id]
优点:
- Leaf服务可以很方便的线性扩展,性能完全能够支撑大多数业务场景
- ID是趋势递增的64位数字
- 高可用:Leaf内部可以使用缓存,即使数据宕机短时间服务仍可用
- 可以自定义max_id和step,便于业务迁移
缺点:
- ID号码不够随机,可能导致发号数量的信息
- TP999数据波动大,当多个Leaf同时消耗完后,还是会阻塞在数据库更新上,业务可能会出现偶尔的时延毛刺
- 强依赖DB,DB宕机会导致系统不可用
双buffer优化
对于第一个缺点,由于是这个方案设计上的问题不能优化了,但对于第二个缺点,可以作进一步的优化,具体思路如下:
之前的方案Leaf从数据库取号段是在号段消耗完的时候进行的,这导致了需要等待从DB取回号段的时间才能返回下一个ID号码,而数据库的操作是比较耗时的,导致Leaf服务阻塞,该次请求时延会突增。
为了解决这个问题,希望两次取号段能尽量做到无缝衔接,那么在号段消耗到某个点(比如100/1000)的时候异步的就请求DB取下一个号段然后保存在内存中,而不是等到号段用完再同步请求DB,这样就可以很大程度减少因为DB阻塞带来的业务抖动,具体实现如下图:
Leaf服务内部采用两个segment buffer,当前号段已下发10%时,如果下一个号段未更新,则启动线程去更新下个号段,这样当buffer1消耗完时buffer2很可能已经更新好了,只需要直接切换当前segment到segment buffer2,然后就可以继续发放号码。两个buffer交替工作,平滑DB带来的I/O阻塞。
数据库高可用容灾
对于第三个缺点强依赖DB的问题,需要DB高可用,可以采用一主两从的方式,分机房部署(常见的架构有“同城三机房”、“两城三中心”),Master和Slave采用半同步复制同步数据,保证至少有两个节点数据一致且不丢失,同时可以接入中间件来实现主从切换。