Shoulder-DB 中提出了高性能序号生成器 Shoulder-Sequence,其实现参考了美团的 leaf、滴滴的 tinyId 等同类技术实现,那这些开源技术有什么区别,该如何选择呢?

滴滴 tinyId:

特点:有极度轻量的客户端(几乎纯java原生)、token 实现、无监控但有监控的扩展设计,客户端可获取当前运行状态

评价:

  • 模型、逻辑清晰,但需要手动初始化 db (代码中无 insert),

  • 序列步幅长度、min、max、loadThreshold 等都在数据库中初始化时配置。

  • 锁:几乎都在初始化中,初始化后,内存号段用完时,同步锁粒度与同类产品更大。

  • 但考虑到tinyId中有到阈值后异步预加载nextBuffer,故 synchronize中大概率只有内存操作,也就不会对性能造成太大影响。

  • 反而减少了大量的代码判断逻辑和循环,易于维护。

  • 滴滴做了很聪明的抉择:但在绝大多数场景下,刚刚提到的内容对于运行时无弊端,反而减少了大量判断逻辑,代码理解更简单可维护性更好。

  • 在绝大多数场景,甚至可以不改代码仅需修改配置直接适用(有基本的鉴权)。

  • 企业大规模适用时,建议自行开发 id 分配器的申请逻辑(insert代码)。

  • 缓存id无过期

  • 提供了静态工具类

美团 leaf:

特点:提供获取 sequence 的 HTTP 接口、提供轻量状态监控、双 buffer 缓冲

评价:

  • 源码为 demo 级别,极度轻量和简化

  • 锁:除了初始化有锁,初始化后,内存号段用完时优先空循环等待

  • 与 tinyId 相比锁力度较细,进入同步前优先调用 waitAndSleep(空循环 1w次 or sleep 10ms)+ volatile 代替 synchronized

  • 逻辑更复杂,在用光 && 异步加载未及时完成 && 并发获取id 情况下性能可能更优(线程未切换),也可能会更差(等待10ms),同时 CPU 占用将更高。

  • 不必太多关注这块性能和实现

  • 缓存id默认每60s过期一次

Shoulder 的 sequence

特点:SpringBoot 开箱即用,提供轻量状态监控、双 buffer 缓冲,细粒度锁、高性能、兼容性强、扩展性好

评价:

  • 与美团 leaf 类似,但有更细粒度的锁,理论并发更好【最追求极限性能】、且判断更完善,定义更清晰,手动指定事物可见性,对不同数据库兼容性更好。

  • 判断是否达到阈值,仅需要由异步线程判断,获取id时无须判断

  • 额外新增定时打印状态

  • 缓存id可定制过期时间

  • 支持多数据库

  • 提供了建议的流水号规则,并考虑到系统升级,版本切换等大型长期运行项目中需要考虑的内容

  • 代码相对更多,多在:

    • 完善的缓存/模型设计

    • 一套代码通过不同配置可兼容不同数据库(如同时有 mysql / oracle)

    • 完善的逻辑判断(各种极端情况)

    • 完善的报错与日志打印

    • 通过配置可指定表名、字段名

    • 更完备的sequence定义:max、step、initValue等

整体比较

  • 性能上几乎都一样

  • 锁的粒度都是 sequenceName 级别,几乎集中在初始化,初始化后 && 合理的参数配置下均几乎无锁

  • 加载实现上均有 双 buffer 缓冲 + 异步检测是否需要加载

  • 使用上几乎一样

  • 几乎都是Generator.nextId()

  • id 缓存略有差别

  • tinyId 缓存id不支持过期

  • leaf 默认过期短,修改需要改代码

  • shoulder 默认过期长,支持自定义

差异:

  • tinyId、leaf 开源是为了提升影响力,提供分布式环境下全局id生成思路。 tinyId 提供了静态工具类,代码更简单,leaf 提供了非常简单的监控接口

  • Shoulder 直接提供了完备的解决方按,不仅仅提供了基础的序列生成能力,还提供了流水号设计的思路,更完善的能力:排查日志、监控埋点、定制数据库名称(便于符合不同组织定义的数据库命名规范)