沉默是金,总会发光
大家好,我是沉默
最近,我们线上系统发生了一起严重事故:订单号和流水号出现了重复,导致核心业务流程受到严重影响。
通过排查,我们发现问题的根源是我们自研的二方包雪花算法ID生成器出现了缺陷。
今天,我们将通过标准雪花算法的结构分析问题所在,并分享一些经验教训和设计建议。
**-**01-
标准雪花算法(Snowflake)
标准的Snowflake ID由一个64位的long型整数构成,具体结构如下:
+----------------------------------------------------------------------------------------------------+| 1 Bit | 41 Bits 时间戳 | 5 Bits 数据中心ID | 5 Bits 机器ID | 12 Bits 序列号 |+----------------------------------------------------------------------------------------------------+
- 1位符号位:始终为0,确保生成的ID为正数。
- 41位时间戳:记录与固定起始时间的毫秒差,可支持约69年的时间。
- 5位数据中心ID:用于标识不同的数据中心。
- 5位机器ID:用于标识不同的节点。
- 12位序列号:在同一毫秒内生成多个ID时使用,最多支持每毫秒生成4096个ID。
优点:
-
高性能生成唯一ID,按时间递增,适合分布式环境。
- 02-
我们的“定制版”雪花算法:问题在哪?
我们使用的二方包雪花算法的结构如下(根据排查推测):
+----------------------------------------------------------------------------------------------------+| 31 Bits 时间戳Delta | 13 Bits 数据中心ID | 4 Bits 工作ID | 8 Bits 业务ID | 8 Bits 序列号 |+----------------------------------------------------------------------------------------------------+
看起来字段丰富,但其实存在严重问题:
- 时间戳仅保留31位,最多支持24.85天
我们自定义的时间戳仅保留了31位,最多支持约24.85天。也就是说,时间戳一旦超过这个范围,会发生溢出,导致ID重复。我们的起始时间是2018年,而2025年时,系统已绕过了好几圈,导致ID的冲突。 - 业务ID使用的是IP的最后一段
我们将IP地址的最后一段作为业务ID(例如192.168.0.1的最后一段1)。这样的设计极其脆弱,在多节点部署时容易发生冲突。 - 工作ID和数据中心ID未配置,全为0
工作ID和数据中心ID未配置且都默认为0,导致所有实例共享同一个节点标识,唯一性保障完全失效。
最终的结果是:时间戳溢出 + IP冲突 + 序列号重复,导致ID严重撞车。
- 03-
教训总结
1. 通用组件不建议自研
雪花算法涉及时间回拨、位运算、分布式协调等复杂问题,不建议自行实现。成熟的开源组件更加稳妥,避免低级错误。
2. 不盲目相信二方包
即便是二方包,也要认真理解其实现原理和唯一性保障机制,确保代码符合自己的业务需求。
3. 合理配置机器ID
使用IP后缀作为机器标识太过脆弱,建议集中规划并统一分配Worker ID和DataCenter ID。
4. 提前覆盖边界场景
通过模拟长时间运行、序列号溢出、时间回拨等极限情况,确保系统能够在各种极端条件下稳健运行。
**-****04-**推荐做法
对于复杂的分布式系统,建议使用成熟的开源实现,如 Hutool 或 Baomidou,以下是它们的示例代码:
Hutool示例:
Snowflake snowflake = IdUtil.getSnowflake(1, 1);long id = snowflake.nextId();
Baomidou示例(支持IP/MAC自动推导,也可手动指定) :
DefaultIdentifierGenerator generator = new DefaultIdentifierGenerator(1, 1); // workerId=1, dataCenterId=1long id = generator.nextId("user");
对于中大型系统,DataCenterId 一般用来标识不同的机房或可用区(AZ)。关于 WorkerId 的配置策略,可以根据系统规模逐步演进,以下是几种推荐配置策略:
- 简单方式:通过配置文件手动指定,适合开发环境或单机部署。
- 标准方式:将IP与端口号(或进程号)拼接后进行哈希,再对WorkerId总数取模,适用于中小规模部署。
- 中级方案:依赖注册中心(如Eureka、Nacos),在服务注册时分配编号,确保唯一性。
- 高级方案:使用Redis、Zookeeper等集中协调WorkerId的分配和释放,支持动态扩容,避免冲突。
随着系统规模扩大,逐步引入更复杂的机制,避免从一开始就过度设计。
**-****05-**其他建议
不要将业务标志拼入ID中
有时为了确保唯一性,我们可能试图将业务信息(如类型前缀、模块编号)拼接进ID。但这样做会带来一些问题:
- 失去排序性:拼接业务标识后,ID可能不再是全数字,失去了按时间递增的排序特性,影响数据库索引效率。
- 存储成本增加:ID的长度变得不规则或偏长,增加了存储成本,并影响日志展示和用户体验。
- 兼容性问题:如果业务字段的含义发生变化,可能会造成数据兼容性问题。
更稳妥的做法是将业务字段单独存储,ID仅用于唯一标识和排序。
总结
不要为造轮子而造轮子,尤其在通用组件上,要避免抱有侥幸心理。如果你也有雪花算法的踩坑经验,欢迎和我们交流分享,共同避免类似的错误。
希望这篇文章能够帮助你规避雪花算法的常见坑,确保你的ID生成系统稳健可靠!
**-****06-**粉丝福利
我这里创建一个程序员成长&副业交流群,
和一群志同道合的小伙伴,一起聚焦自身发展,
可以聊:
技术成长与职业规划,分享路线图、面试经验和效率工具,
探讨多种副业变现路径,从写作课程到私活接单,
主题活动、打卡挑战和项目组队,让志同道合的伙伴互帮互助、共同进步。
如果你对这个特别的群,感兴趣的,
可以加一下, 微信通过后会拉你入群,
但是任何人在群里打任何广告,都会被我T掉。