分布式 ID 能否保证全局递增?
简化版
无中心协调的分布式 ID 通常只能保证唯一和趋势递增,不能保证按真实业务时间严格全局递增。Snowflake 可保证单 worker 在时钟不回拨时单调,跨 worker 会受同毫秒序列、WorkerId 和时钟偏差影响。 若业务真的需要全局连续顺序,就必须串行化到单一 sequencer/共识日志,代价是延迟、吞吐和可用性。
详细版
先拆需求:数据库索引只需大致有序;每实体事件顺序可用业务版本;全局审计顺序需要提交日志;“无空洞连续号”还会受事务回滚影响,成本最高。不能拿 ID 数字大小替代因果或提交顺序。
Snowflake 时间高位让不同时间段大体递增,但 worker A 时钟快 20ms 时,稍后发生在 B 的事件可能 ID 更小。号段模式中实例 A 拿 [1,1000]、B 拿 [1001,2000],B 先用完也会产生生成顺序与数字顺序不一致。
完整版教学
一、“有序”至少有四种含义
| 需求 | 含义 | 常见方案 |
|---|---|---|
| 单节点单调 | 后一个 ID 更大 | 本地计数/Snowflake |
| 趋势递增 | 时间大体对应大小 | Snowflake、UUIDv7 |
| 全局提交序 | 所有事件有唯一顺序 | 共识日志/sequencer |
| 连续无空洞 | 1,2,3…不缺号 | 串行分配并处理回滚 |
需求没说清时,直接承诺“全局递增”通常是在埋雷。
记忆钩子:唯一回答“会不会撞”,有序回答“谁先谁后”,两者不是同一个保证。
二、Snowflake 为什么只是趋势递增
经典布局时间戳在高位,同一 worker 内序列递增,因此本节点单调。跨 worker 同毫秒生成时,数字顺序由 WorkerId 位决定,而非请求先后;时钟偏差还会让后发生事件使用更小时间戳。
real time: A event at 10:00:00.020, clock=+30ms -> ts=50
real time: B event at 10:00:00.030, clock=0ms -> ts=30
B 后发生,但 ID 可能更小
三、号段也不保证生成顺序
实例 A 先领取 1~1000 后暂停,实例 B 领取 1001~2000 并立即发出 1001。稍后 A 恢复发出 1,此时生成时间顺序与 ID 大小相反。增大步长会减少数据库请求,却会扩大这种乱序和空洞。
所以号段 ID 适合唯一、高吞吐和数据库整数主键,不适合作为全局事件时间线。
四、严格顺序为何需要协调
要让所有参与者同意唯一顺序,必须把请求提交到单 Leader/sequencer 或复制共识日志。每个 ID 只有在前一位置确定后才可确认,网络分区时少数侧不能继续。这本质上以协调延迟和可用性换顺序。
可用批量预分提高吞吐,但一旦多个实例并行消费批次,又失去实时生成顺序。顺序保证与去中心化高吞吐存在根本张力。
五、连续无空洞为什么更贵
即使中心发出 101,业务事务随后回滚,101 就形成空洞。若要求票据号无空洞,通常要把编号分配放到业务提交阶段,串行锁定,作废号码也要保留审计而非重用。很多监管场景需要的是“可解释的连续凭证”,不是数据库主键。
重用回滚号码会在并发和故障恢复中造成碰撞,通常不值得。
六、用正确字段表达业务顺序
订单状态用 version 做单实体 CAS,消息分区用 offset 表示分区顺序,跨实体因果用事件版本/向量时钟,全局账本用共识日志位置。created_at 与 ID 可用于展示排序,但冲突时应有稳定 tie-breaker,并不宣称因果。
监控时钟偏差、ID 倒退、WorkerId 冲突和排序依赖。代码审查要禁止从 Snowflake 反推严格创建顺序或拿 max(id) 当最新业务状态。
七、常见误区与追问
- 误区:Snowflake 时间在高位,所以全局严格递增。 跨 worker 时钟和序列无法保证真实先后。
- 误区:数据库自增一定连续无空洞。 事务回滚、缓存和删除都会产生空洞。
- 误区:号段区间递增就代表生成顺序递增。 各实例消费速度不同会乱序。
- 误区:ID 更大就一定是更新版本。 业务提交、复制和时钟都可能改变顺序。
- 追问:索引性能需要哪种有序? 趋势递增通常足够,无需全球串行。
- 追问:每个订单事件如何排序? 使用订单内 version/sequence,比全局 ID 更准确。
- 追问:全局审计顺序怎么办? 使用单一账本或共识日志位置,并接受协调代价。
八、加强记忆
回答时先拆“单调、趋势、全局、连续”四级需求。Snowflake 与号段擅长唯一和高吞吐,只提供有限有序;严格全局顺序需要 sequencer/共识,连续无空洞还要绑定业务提交。业务因果应由版本和日志表达,不从 ID 大小猜。