数据库分库分表实现分布式ID的解决方案

目前的方案:

  • UUID
  • 数据库自增
  • 号段模式
  • Redis实现
  • 雪花算法(Snowflake)
  • 百度Uidgenerator
  • 美团Leaf
  • 滴滴TinyID

分布式 ID 的要求:

  • 全局唯一性:不能出现重复的ID号,既然是唯一标识,这是最基本的要求。
  • 趋势递增:在MySQL InnoDB引擎中使用的是聚集索引,由于多数RDBMS使用B-tree的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写入性能。
  • 单调递增:保证下一个ID一定大于上一个ID,例如事务版本号、IM增量消息、排序等特殊需求。
  • 信息安全:如果ID是连续的,恶意用户的扒取工作就非常容易做了,直接按照顺序下载指定URL即可;如果是订单号就更危险了,竞对可以直接知道我们一天的单量。所以在一些应用场景下,会需要ID无规则、不规则。

来自 https://tech.meituan.com/2017/04/21/mt-leaf.html

UUID:

通用唯一识别码, 标准型包含32个16进制数字, 大小一共 128bit, 有效保证唯一性

  • 优点: 本地生成计算生成, 效率极高, 有效保证唯一性
  • 缺点: 无序, 不满足趋势递增和单调递增, 写入效率极低, 并且基于 mac 地址的 UUID 还容易导致信息泄露

数据库自增:

简单易懂一张表:

数据库DB_1DB_2DB_3
主键ID /step=30,0+3,0+6,0+9,...1,1+3,1+6,1+9,...2,2+3,2+6,2+9,...
  • 优点:

    • 生成效率极高, 数据库自己实现;
    • 写入效率高, 自增 ID 的写入是追加写入;
  • 缺点:

    • 数据库也无法做拓展, 一旦目前的数据库不够用就只能全部数据迁移
    • ID 只有趋势上是递增的, 但是实际上可能会存在后进入的数据的 ID 小于以前进入的数据的 ID ( 原因是每个数据库的 ID 各自独立自增的没有任何联系 )

号段模式:

​ 实现思路是会从数据库根据数据获取一个号段范围,比如[1,1000],生成1到1000的自增ID加载到内存中, 等 ID 都用了, 再去数据库获取, 然后更改最大值, 这种模式依赖于数据库,但是区别于数据库主键自增的模式。假设1000为一个号段1000,2000,3000,每一次 I/O 可以获得1000个 ID,性能显著提高。段号用完了再去取, 降低数据库压力。

Redis 辅助实现

​ 基于 Redis 的自增原子指令 INCRINCRBY , 并且由于 Redis 的单线程特点, 保证了 ID 的唯一性和自增有序性

  • 优点: Redis 新能好, 效率高
  • 缺点: 很依赖 Redis 性能, 在高并发场景下就需要集群,不过集群后,又要和传统数据库一样,设置分段和步长

雪花算法:

​ 由Twitter开源的分布式ID生成算法,以划分命名空间的方式将 64-bit位分割成多个部分,每个部分代表不同的含义, 通常为

符号位时间戳(ms级)工作机器id序列号
1bit / 默认 041bit10bit12bit

ID 长度64位, java 中用 Long 储存, 所以有一个符号位, 这样也可以保证 long 型的 ID 也是自增的

时间错精切到毫秒级别, 41bit可以表示69年数据, 暂时够用.

  • 优点:
    • ID是趋势递增,不依赖数据库等第三方,生成ID的效率非常高,稳定性好;
    • 可以根据自身业务特性分配bit位,比较灵活
  • 缺点:
    • 强依赖服务器上的机器时钟, 如果时钟回拨, 则会导致ID重复, 服务不可用

企业级方案:

如无必要, 勿增实体
1. 百度 Uidgenerator

来自 Github 介绍:

​ UidGenerator是Java实现的, 基于Snowflake算法的唯一ID生成器。UidGenerator以组件形式工作在应用项目中, 支持自定义workerId位数和初始化策略, 从而适用于docker等虚拟化环境下实例自动重启、漂移等场景。 在实现上, UidGenerator通过借用未来时间来解决sequence天然存在的并发限制; 采用RingBuffer来缓存已生成的UID, 并行化UID的生产和消费, 同时对CacheLine补齐,避免了由RingBuffer带来的硬件级「伪共享」问题. 最终单机QPS可达600万。

2. 美团 Leaf

有两种方案: Leaf-segment 和 Leaf-snowflake

Leaf-segment 方案:

img

优化:

  • 原方案每次获取ID都得读写一次数据库,造成数据库压力大。改为利用 proxy server 批量获取,每次获取一个segment(step决定大小)号段的值。用完之后再去数据库获取新的号段,可以大大的减轻数据库的压力。
  • 各个业务不同的发号需求用 biz_tag 字段来区分,每个biz-tag的ID获取相互隔离,互不影响。如果以后有性能需求需要对数据库扩容,不需要上述描述的复杂的扩容操作,只需要对biz_tag分库分表就行。
  • 双 Buffer 无阻塞优化, 不需要在DB取号段的时候阻塞请求线程,即当号段消费到某个点时就异步的把下一个号段加载到内存中。而不需要等到号段用尽的时候才去更新号段。Leaf服务内部有两个号段缓存区segment。当前号段已下发10%时,如果下一个号段未更新,则另启一个更新线程去更新下一个号段。当前号段全部下发完后,如果下个号段准备好了则切换到下个号段为当前segment接着下发,循环往复。

img

Leaf-snowflake 方案:

优化:

  • 弱依赖 ZooKeeper 实现 WorkID 的自动注册和分配, 每个节点也会自己缓存自己的 WorkId 避免 ZooKeeper 挂掉导致服务挂掉

img

  • 增加了时钟校验的逻辑, 防止机器时钟回拨带来的问题

img

3. 滴滴TinyID

Code架构

Segment 方法实现分布式 ID, 但是做了以下优化:

  • 双 buffer 缓存, 避免阻塞
  • 支持多 db 集群, 每次获取段号都随机从一个存活的数据库上拿, 这样就可以提高容灾性能, 只要有一个 db 没有挂就可以继续用
idbiz_typemax_idstepdeltaremainderversion
1100020001000200

​ delta代表id每次的增量,remainder代表余数,例如可以将A,B都delta都设置2,remainder分别设置为0,1则,A的号段只生成偶数号段,B是奇数号段。 通过delta和remainder两个字段我们可以根据使用方的需求灵活设计db个数,同时也可以为使用方提供只生产类似奇数的id序列。

  • 增加了一个 client 用于本地缓存段号, 不用每次需要拿到 ID 都给服务器发送一个 http 请求, 提高效率

最终架构