当前位置: 首页 > news >正文

【架构实战】分布式ID生成:从自增ID到雪花算法

【架构实战】分布式ID生成:从自增ID到雪花算法

一、背景:分库分表后,自增ID的噩梦

2019年我们把订单表做了分库分表,按用户ID哈希分了16个库。

上线第二天,运营反馈:后台看到的订单号会跳变。查了一下午才搞清楚:MySQL自增ID在每个库里独立计数,导致不同分库里的订单ID重复了。

分库前(单库): orders 表: AUTO_INCREMENT → 1001, 1002, 1003... 全局唯一 分库后(16个库): db_00.orders: AUTO_INCREMENT → 1001, 1002, 1003... db_01.orders: AUTO_INCREMENT → 1001, 1002, 1003... ← 与db_00重复了!

那天我们手动跑脚本给所有订单号加了个库前缀才紧急修复。但这只是临时方案,根本问题没解决。

分库分表之后,数据库自增ID彻底失效,必须要有全局唯一的ID生成方案。


二、分布式ID的核心要求

分布式ID必须满足: 1. 全局唯一:最基本要求,任何两个ID不能重复 2. 趋势递增:索引友好,减少B+树页分裂 3. 高性能:生成速度不能成为系统瓶颈 4. 高可用:ID生成服务不能有单点故障 5. 信息安全:不能暴露业务量(如今天第几个订单)

三、方案演进:从青铜到王者

3.1 青铜:UUID

Stringid=UUID.randomUUID().toString();// 例如:550e8400-e29b-41d4-a716-446655440000

优点:本地生成,无网络开销,实现超简单。

缺点(致命)

  • 字符串太长(36字节),占用大量存储空间
  • 完全无序,作为主键会导致B+树频繁页分裂,写入性能下降50%以上
  • 不包含时间信息,不便于排查问题
UUID作为主键插入B+树: 每次插入都是随机位置 → 页分裂频繁 → 磁盘碎片 → 性能灾难 趋势递增ID作为主键插入B+树: 每次插入到最后 → 极少页分裂 → 顺序写入 → 性能优秀

结论:可以用作业务编号但绝不适合做数据库主键。

3.2 白银:数据库号段模式

原理:用一个DB表集中分配ID号段,服务每次批量获取一段ID,用完后再次申请。

-- ID号段表CREATETABLEid_generator(biz_typeVARCHAR(64)PRIMARYKEYCOMMENT'业务类型',max_idBIGINTNOTNULLCOMMENT'当前已分配的最大ID',stepINTNOTNULLDEFAULT1000COMMENT'每次分配的号段长度',versionINTNOTNULLDEFAULT0COMMENT'乐观锁版本号');-- 获取号段的原子操作UPDATEid_generatorSETmax_id=max_id+step,version=version+1WHEREbiz_type='order'ANDversion=#{oldVersion};-- 如果update影响行数>0 → 分配成功,号段 = [max_id - step + 1, max_id]
@ServicepublicclassDbSegmentIdGenerator{privatevolatileSegmentcurrentSegment;privatevolatileSegmentnextSegment;// 双Buffer优化@PostConstructpublicvoidinit(){currentSegment=fetchSegment();nextSegment=fetchSegment();// 预加载下一个号段// 异步预加载线程Executors.newSingleThreadScheduledExecutor().scheduleWithFixedDelay(this::preloadSegment,0,1,TimeUnit.SECONDS);}publicsynchronizedlongnextId(){if(!currentSegment.hasNext()){// 号段用完,切换到预加载的号段currentSegment=nextSegment;nextSegment=fetchSegment();}returncurrentSegment.next();}privateSegmentfetchSegment(){// 执行 UPDATE id_generator SET max_id = max_id + step// 返回号段 [newMaxId - step + 1, newMaxId]}}

优点:ID趋势递增,本地缓存号段性能高,每次DB交互可分配1000个ID。

缺点:依赖DB,DB宕机时号段耗完后无法分配;多业务需要多张表。

3.3 黄金:Redis生成

// 使用Redis的INCRBY原子操作longid=redisTemplate.opsForValue().increment("id:order",1);// 每次+1,返回新ID

优点:性能极高,实现简单。

缺点

  • Redis不持久化会丢ID(重启后从0开始)
  • Redis持久化(RDB/AOF)有性能损耗
  • 主从切换可能产生重复ID

3.4 王者:雪花算法(Snowflake)

原理:一个64位Long整数,各段含义如下:

┌─┬──────────────────────────────────────────────┬──────────┬────────────┐ │0│ 41位时间戳(毫秒) │ 10位机器码│ 12位序列号 │ └─┴──────────────────────────────────────────────┴──────────┴────────────┘ 1位 41位 10位 12位 (保留) 各部分说明: - 第1位:固定0(保证正数) - 41位时间戳:从自定义起始时间到现在的毫秒数,可用69年 - 10位机器码:最多1024个节点(5位机房+5位机器ID) - 12位序列号:同一毫秒内最多4096个ID QPS = 4096个/毫秒 × 1000 = 约400万/秒
publicclassSnowflakeIdGenerator{// 起始时间戳(2020-01-01 00:00:00)privatestaticfinallongSTART_TIMESTAMP=1577836800000L;// 各部分位数privatestaticfinallongDATA_CENTER_ID_BITS=5L;// 5位机房IDprivatestaticfinallongWORKER_ID_BITS=5L;// 5位机器IDprivatestaticfinallongSEQUENCE_BITS=12L;// 12位序列号// 最大值privatestaticfinallongMAX_DATA_CENTER_ID=~(-1L<<DATA_CENTER_ID_BITS);// 31privatestaticfinallongMAX_WORKER_ID=~(-1L<<WORKER_ID_BITS);// 31privatestaticfinallongMAX_SEQUENCE=~(-1L<<SEQUENCE_BITS);// 4095// 偏移量privatestaticfinallongWORKER_ID_SHIFT=SEQUENCE_BITS;// 12privatestaticfinallongDATA_CENTER_ID_SHIFT=SEQUENCE_BITS+WORKER_ID_BITS;// 17privatestaticfinallongTIMESTAMP_SHIFT=SEQUENCE_BITS+WORKER_ID_BITS+DATA_CENTER_ID_BITS;// 22privatefinallongdataCenterId;privatefinallongworkerId;privatelongsequence=0L;privatelonglastTimestamp=-1L;publicSnowflakeIdGenerator(longdataCenterId,longworkerId){if(dataCenterId>MAX_DATA_CENTER_ID||dataCenterId<0){thrownewIllegalArgumentException("机房ID超出范围: 0-"+MAX_DATA_CENTER_ID);}if(workerId>MAX_WORKER_ID||workerId<0){thrownewIllegalArgumentException("机器ID超出范围: 0-"+MAX_WORKER_ID);}this.dataCenterId=dataCenterId;this.workerId=workerId;}publicsynchronizedlongnextId(){longcurrentTimestamp=System.currentTimeMillis();// 时钟回拨检测if(currentTimestamp<lastTimestamp){thrownewRuntimeException("时钟回拨: 上次时间="+lastTimestamp+", 当前时间="+currentTimestamp);}if(currentTimestamp==lastTimestamp){// 同一毫秒内,序列号递增sequence=(sequence+1)&MAX_SEQUENCE;if(sequence==0){// 序列号用完,等待下一毫秒currentTimestamp=waitNextMillis(lastTimestamp);}}else{// 不同毫秒,序列号重置sequence=0L;}lastTimestamp=currentTimestamp;// 组装IDreturn((currentTimestamp-START_TIMESTAMP)<<TIMESTAMP_SHIFT)|(dataCenterId<<DATA_CENTER_ID_SHIFT)|(workerId<<WORKER_ID_SHIFT)|sequence;}privatelongwaitNextMillis(longlastTimestamp){longtimestamp=System.currentTimeMillis();while(timestamp<=lastTimestamp){timestamp=System.currentTimeMillis();}returntimestamp;}}

3.5 雪花算法的时钟回拨问题

问题:服务器时钟被调回过去,可能产生重复ID。

时间线: 10:00:00.000 → 生成了ID=100 10:00:00.001 → 生成了ID=101 突然NTP同步,时钟回拨到 09:59:59.500 10:00:00.000 → 又生成了ID=100 ← 重复了!

解决方案

// 方案1:短期回拨等待privatesynchronizedlongnextIdWithFallback(){longcurrentTimestamp=System.currentTimeMillis();if(currentTimestamp<lastTimestamp){longoffset=lastTimestamp-currentTimestamp;if(offset<=5){// 回拨5ms以内,等待追平try{Thread.sleep(offset+1);}catch(InterruptedExceptione){Thread.currentThread().interrupt();}currentTimestamp=System.currentTimeMillis();}else{// 回拨超过5ms,使用备用workerIdreturngenerateWithBackupWorkerId(currentTimestamp);}}// ... 正常生成逻辑}// 方案2:记录最后生成的ID,时钟回拨时用历史最大ID+1// 方案3:放弃本节点ID生成,切换到备用ID生成器

生产环境推荐:使用美团Leaf或百度UidGenerator,它们已解决时钟回拨问题。


四、方案对比

方案唯一性趋势递增性能(QPS)高可用运维复杂度适用场景
UUID极高0非DB主键场景
DB号段中(依赖DB)中小规模
Redis中等已有Redis
雪花原生极高(400万/s)大型分布式
美团Leaf极高企业级

五、美团Leaf方案简介

美团开源了两种模式:

Leaf-Segment(号段模式): ┌─────────┐ ┌─────────┐ │ Leaf-Svr│ │ Leaf-Svr│ │ (主) │ ←─→ │ (从) │ └────┬────┘ └────┬────┘ │ │ ▼ ▼ ┌─────────┐ ┌─────────┐ │ 服务A │ │ 服务B │ │ (获取 │ │ (获取 │ │ 号段) │ │ 号段) │ └─────────┘ └─────────┘ - 双Buffer预加载:当前号段用完前已预加载下一个 - 高可用:多个Leaf Server组成集群 Leaf-Snowflake(雪花模式): - 通过ZooKeeper自动注册workerId - 内置时钟回拨解决方案

六、总结

方案选择决策树

你的规模? │ ├── 单库单表 → 直接MySQL自增ID │ ├── 分库分表 < 10个节点 → DB号段模式 │ └── 优点:实现简单,趋势递增,索引友好 │ ├── 分库分表 > 10个节点 → 雪花算法 │ └── 优点:高性能,高可用,本地生成 │ └── 企业级 > 100个节点 → 美团Leaf └── 优点:成熟方案,高可用,时钟回拨已解决

三条关键经验

  1. 不要用UUID做主键:B+树的噩梦,写入性能下降50%以上
  2. 雪花算法的workerId不能手动配置:机器多了必出错,用ZK或DB自动分配
  3. 时钟回拨是真实存在的问题:NTP同步、虚拟机迁移都可能触发,生产环境必须处理

分布式ID看起来简单,却是分库分表后第一个遇到的坑。方案选对,事半功倍;方案选错,数据迁移就是噩梦。


个人观点,仅供参考

http://www.jsqmd.com/news/1152792/

相关文章:

  • 2026泳池瓷砖胶怎么选?长期浸水环境粘结力衰减测试
  • # 探针卡笔记|002
  • MyBatis Generator:根据数据库表生成 MyBatis 的实体类、Mapper 接口和 XML 映射文件
  • 如何选择?揭秘5大靠谱心理设备企业,让你不再迷茫
  • 【ROS2】 机械臂架构 vs UE5 游戏开发架构 终极对照表
  • 基于51/STM32单片机的激光测距仪 防撞报警 倒车雷达 嵌入式套件31(设计源文件+万字报告+讲解)(支持资料、图片参考_相关定制)_
  • Cursor Free VIP深度解析:突破AI编程助手限制的完整技术方案
  • Python 4种插值方法对比:Griddata vs Krige vs RBF vs IDW,2000+站点实测效率与效果
  • 2026年邮件营销高依赖行业TOP10?一文看懂热门应用场景
  • Windows防撤回技术深度解析:RevokeMsgPatcher如何让消息永不消失
  • 豆包千问相继收紧智能体功能,本地部署才是Agent的地基
  • 电阻式传感器选型指南:3类核心原理对比与5个工业应用场景解析
  • 2026年AI大模型API加速站全网真实实测:五大主流平台硬核数据全维度对比选型攻略
  • 挑战/响应认证实战:Python 3.11 模拟 3 种攻击场景与防御方案
  • 用MCUXPRESSO IDE 仿真 MIMXRT1186失败的bug
  • 为什么“记住规律“比“记住事实“更重要? 规律可以极大地压缩信息 *提炼规律、建立联系、在需要时快速重建细节
  • YOLOv8疲劳驾驶识别检测系统(项目源码+YOLO数据集+模型权重+UI界面+python+深度学习+环境配置+目标检测)
  • Flink 2.x状态演进:理解解 1.x 与 2.x 状态存储机制
  • 安卓短信备份恢复终极指南:SMS Backup+ 完整使用教程
  • 全过程咨询、工程投标数字化工具分享|一站式招标资讯小程序全咨数字平台实操指南
  • 在章贡区找新房装修企业,到底哪家才真正可靠呢?
  • 侧翻自正,稳行波浪:智能型水上遥控救援飞翼
  • 基于SpringBoot的智慧农场农事管理系统设计与实现
  • 【Midjourney vs DALL-E终极对决】:20年AI视觉工程师实测12项核心指标,谁才是商业级生成的真正王者?
  • okbiye|开题报告 AI 专属创作模块,破解毕业生开题构思全阶段难题
  • 基于Dify工作流与MCP协议构建企业岗位智能副驾实战指南
  • 新人量化软件避坑:看懂参数再看策略结果
  • LangChain 零基础入门教程|Python + Node.js 双版本实战
  • 2026年,揭秘苦荞挂面背后的匠心与健康秘密
  • 终极免费解锁Cursor Pro完整功能:告别AI编程助手限制的完整指南