ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

分布式 ID 生成:雪花算法与号段模式实战

分布式 ID 生成:雪花算法与号段模式实战 分布式 ID 生成雪花算法与号段模式实战背景与动机单体时代我们习惯用数据库自增主键AUTO_INCREMENT给每行数据编号。它简单、有序、占用空间小。但一旦进入分布式与分库分表场景这套方案立刻失灵跨库无法保证全局唯一订单表按用户 ID 取模拆成 8 个库每个库的id都从 1 自增合并后必然撞号。分库分表后无法做全局排序自增主键隐含时间序跨库后这个序被打破按id翻页会出现错乱。暴露业务量连续 ID 让竞对轻易算出你每天的注册量、订单量存在信息泄漏风险。数据库成为瓶颈高并发写入时每次都要找库拿号DB 压力陡增。于是我们需要一种全局唯一、趋势递增、高可用、低延迟的分布式 ID 方案。业界主流有两类思路一类是算法派本地计算如雪花算法 Snowflake一类是集中发号派DB/中间件统一分配如号段模式 Leaf-segment。下面逐一拆解并给出可运行实现。核心概念与技术选型先澄清几个 ID 的核心指标全局唯一最基本要求。趋势递增不一定严格递增但整体单调递增方便 BTree 索引顺序写、避免页分裂。单调递增严格递增便于排序但可能暴露流量。高可用 低延迟发号服务不能成为链路单点且耗时应在微秒级。常见方案对比方案唯一性趋势递增性能依赖主要缺点UUID极高否无序高本地无36 字符太长、无序导致索引碎片化数据库自增单库单库内唯一是低IODB无法跨库、有单点RedisINCR高是高Redis强依赖 Redis 可用性、宕机可能重复雪花算法 Snowflake高是极高本地时钟时钟回拨问题、workerId 分配难号段模式 Leaf-segment高是高双缓冲DB号段用尽瞬间有 DB 抖动、号段浪费选型结论对绝大多数业务雪花算法是零依赖、低延迟的首选当需要严格可控、可审计、不依赖时钟的场景如金融单据号号段模式更稳。两者可以并存核心交易走号段日志/埋点走雪花。分步实现过程方案一雪花算法Java 实现雪花算法把 64 位 long 拆成1 位符号位(0) | 41 位时间戳 | 10 位机器 ID | 12 位序列号。每秒可生成约 409.6 万个 ID。publicclassSnowflakeIdGenerator{// 起始时间戳可自定义为项目启动时间能多撑几十年privatefinallongepoch1700000000000L;privatefinallongworkerIdBits10L;privatefinallongsequenceBits12L;privatefinallongmaxWorkerId~(-1LworkerIdBits);// 1023privatefinallongworkerIdShiftsequenceBits;// 12privatefinallongtimestampShiftsequenceBitsworkerIdBits;// 22privatefinallongsequenceMask~(-1LsequenceBits);// 4095privatefinallongworkerId;privatelongsequence0L;privatelonglastTimestamp-1L;publicSnowflakeIdGenerator(longworkerId){if(workerId0||workerIdmaxWorkerId){thrownewIllegalArgumentException(workerId 超出范围 0~maxWorkerId);}this.workerIdworkerId;}publicsynchronizedlongnextId(){longtimestampSystem.currentTimeMillis();// 时钟回拨直接抛异常也可改为等待见下文权衡if(timestamplastTimestamp){thrownewRuntimeException(时钟回拨拒绝生成 ID);}if(timestamplastTimestamp){// 同一毫秒内序列号自增sequence(sequence1)sequenceMask;if(sequence0){// 当前毫秒序列用尽自旋等到下一毫秒timestampwaitNextMillis(lastTimestamp);}}else{sequence0L;// 进入新毫秒序列号归零}lastTimestamptimestamp;// 拼接时间戳左移 22 机器左移 12 序列return((timestamp-epoch)timestampShift)|(workerIdworkerIdShift)|sequence;}privatelongwaitNextMillis(longlastTs){longtsSystem.currentTimeMillis();while(tslastTs){tsSystem.currentTimeMillis();}returnts;}}调用方只需new SnowflakeIdGenerator(workerId).nextId()本地计算、无网络开销。方案二号段模式Leaf-segment核心思想DB 里存每张业务的当前号段max_id与步长step应用一次拉取一整段如 1000 个到内存用完了再去 DB 取下一个段。配合双缓冲几乎消除取号时的 DB 抖动。建表CREATETABLEid_segment(biz_tagVARCHAR(64)PRIMARYKEY,-- 业务标识如 ordermax_idBIGINTNOTNULL,-- 当前已分配号段的上界stepINTNOTNULL,-- 每次拉取的步长versionINTNOTNULL-- 乐观锁防并发覆盖);Java 取号简化版含双缓冲思路publicclassSegmentIdGenerator{// 双缓冲当前段用 buffer1快用完时异步加载 buffer2privatevolatileSegmentcurrent;privatevolatileSegmentnext;privatefinalintstep1000;staticclassSegment{longcur;// 当前游标longmax;// 本段上界booleanloading;}publicsynchronizedlongnextId(StringbizTag){// 1. 先用当前段发号if(current.curcurrent.max){// 2. 当前段已空且下一段就绪则切换否则同步去 DB 取if(next!null){currentnext;nextnull;}else{loadFromDb(bizTag);}}// 3. 使用量超过 80%提前异步加载下一段避免用尽时阻塞if(!current.loading(current.max-current.cur)step*0.2){asyncLoadNext(bizTag);}returncurrent.cur;}// 关键点UPDATE ... SET max_id max_id step WHERE biz_tag ?// 用 version 乐观锁或行锁保证并发安全取回后本地 max 旧 max stepprivatevoidloadFromDb(StringbizTag){/* DB 原子更新并取回新段上界 */}privatevoidasyncLoadNext(StringbizTag){/* 线程池异步预取 */}}loadFromDb的本质是一条UPDATE id_segment SET max_id max_id step, version version1 WHERE biz_tag ? AND version old取回成功后本地段的max即为新上界避免了先读后写的并发冲突。踩坑、边界条件与权衡取舍1. 雪花算法的时钟回拨这是头号坑。NTP 同步、虚拟机休眠恢复都可能让系统时间往回跳。直接抛异常会导致短暂不可用更稳妥的做法是回拨幅度小如 5ms时自旋等待幅度大时切换备用 workerId 或报警人工介入。绝对不能为了不报错而把时间戳强行往回取序列号否则会生成与历史重复的 ID。2. workerId 如何分配10 位机器位最多支持 1024 个节点但 workerId 不能写死在配置里扩缩容必撞。推荐启动向注册中心如 ZooKeeper / Nacos申请一个有序临时节点序号或基于IP 端口哈希取模容器化环境建议通过环境变量注入避免 Pod 重建后重复。3. 号段浪费与 DB 抖动如果某业务号段step1000但一天只用 50 个服务重启后未用的 950 个就永久浪费ID 不连续可接受但审计需注意。同时首次取号和段切换临界会有一次 DB 往返双缓冲把它降到几乎无感但 DB 本身必须高可用否则发号链路全断。4. 趋势递增 ≠ 严格递增雪花算法在时钟回拨修复、workerId 不同节点间可能出现后发的 ID 更小的局部乱序。若你的下游强依赖严格有序如按 ID 做binlog 顺序消费需额外加逻辑或改用号段模式。5. 时间位耗尽41 位毫秒时间戳约可用 69 年从自定义 epoch 起算。务必把epoch设为项目起始时间而非 1970否则可用年限被白白浪费一半。小结与延伸阅读分布式 ID 没有银弹只有适配场景优先雪花算法无依赖、低延迟、趋势递增适合绝大多数内部业务主键、日志追踪 ID。选号段模式需要可控、可审计、规避时钟问题的金融/单据类场景配合双缓冲保性能。避坑要点时钟回拨处理、workerId 动态分配、epoch 合理设置、号段双缓冲与步长评估。落地时建议做一个统一发号 SDK把两种算法封装在背后业务方只调用IdGenerator.next(order)未来替换算法不侵入业务代码。延伸阅读Twitter Snowflake 原始设计snowflake-2010美团 Leaf 开源实现Leaf-segment / Leaf-snowflake 双模式百度 UidGenerator基于 Snowflake 的改进用Delta seconds worker sequence结构《数据密集型应用系统设计》DDIA中分布式唯一标识相关章节
返回列表