分布式 ID 生成方案

分库分表之后数据库自增主键不再全局唯一——每个分片都从 1 开始,订单号会撞车。即使没分库,也需要一个能对外暴露、不泄露业务量、趋势递增的订单号。分布式 ID 解决的就是"多节点如何生成全局唯一 ID"的问题。

一个好 ID 的标准

要求说明不满足的后果
全局唯一任何节点、任何时刻都不重复数据覆盖、主键冲突
趋势递增整体随时间增大B+ 树随机插入,页分裂,写入变慢
高性能满足峰值写入 QPSID 生成成为写入瓶颈
高可用不因单点故障停止发号下单链路整体不可用
信息安全外部不可推算业务量竞对能估算你的日订单量

"趋势递增"不等于"严格单调递增":只要求整体随时间变大,允许同毫秒内乱序,这对 B+ 树已经足够友好。

方案一:数据库自增与步长

最省事的做法是直接用数据库自增,缺点是强依赖数据库、每次取号一次数据库往返:

CREATE TABLE id_seq (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  stub CHAR(1) NOT NULL DEFAULT 'x',
  PRIMARY KEY (id), UNIQUE KEY uk_stub (stub)
) ENGINE=InnoDB;

REPLACE INTO id_seq (stub) VALUES ('x');
SELECT LAST_INSERT_ID();   -- 输出:1024

单点问题可以靠"多实例不同起始值 + 步长"错开:

-- 实例 1(得到 1, 4, 7 ...):SET @@auto_increment_offset = 1; SET @@auto_increment_increment = 3;
-- 实例 2(得到 2, 5, 8 ...):SET @@auto_increment_offset = 2; SET @@auto_increment_increment = 3;

方案二:UUID

// 片段,需放入工程
String id = UUID.randomUUID().toString();
// 输出示例:f47ac10b-58cc-4372-a567-0e02b2c3d479(36 个字符)

优点是本地生成、无网络与中心依赖、性能极高、几乎不会冲突;缺点是完全无序,作主键会让 B+ 树频繁页分裂,且 36 个字符比 BIGINT 大数倍,索引膨胀明显。折中做法:UUID 作为对外唯一标识(如 traceId),不要作为 InnoDB 聚簇索引主键

方案三:雪花算法(Snowflake)

把 64 位长整型切成三段:时间戳保证趋势递增,机器位保证多节点不冲突,序列号保证同毫秒内不重复。

 0 | 41 bit 毫秒时间戳 | 10 bit 机器 ID | 12 bit 序列号
符号位  约可用 69 年       最多 1024 台    每毫秒 4096 个 → 单机峰值约 409.6 万 ID/秒
nextId():
    now = currentTimeMillis()
    if now < lastTimestamp: return handleClockBackwards(lastTimestamp - now)  # 时钟回拨
    if now == lastTimestamp:
        sequence = (sequence + 1) & 4095
        if sequence == 0: now = waitNextMillis(lastTimestamp)   # 本毫秒 4096 个用完
    else: sequence = 0
    lastTimestamp = now
    return (now - epoch) << 22 | (workerId << 12) | sequence

时钟回拨的处理策略:

回拨幅度处理方式
几毫秒自旋等待时钟追上 lastTimestamp 再发号
中等及以上抛异常拒绝发号由上层重试,或切换备用位、降级到号段模式

优点是本地生成、性能极高、趋势递增;限制是强依赖系统时钟、机器 ID 需统一分配、时间戳位数决定可用年限约 69 年。生产环境务必部署 NTP 时钟同步并监控回拨事件。

方案四:号段模式

应用启动或号段用尽时,从数据库领取一个区间(如 1~1000),之后在内存里自增,用完再领下一段。

CREATE TABLE id_segment (
  biz_tag  VARCHAR(32) NOT NULL COMMENT '业务标识,如 order',
  max_id   BIGINT NOT NULL DEFAULT 0 COMMENT '当前已分配到的最大值',
  step     INT NOT NULL DEFAULT 1000 COMMENT '每次领取的号段长度',
  PRIMARY KEY (biz_tag)
) ENGINE=InnoDB;

-- UPDATE 原子累加,再读回本段的边界值
UPDATE id_segment SET max_id = max_id + step WHERE biz_tag = 'order';
SELECT max_id, step FROM id_segment WHERE biz_tag = 'order';
nextId():
    if next > end:                       # 当前段用尽,领取新段
        新段 = 从数据库领取(max_id += step)
        next = 新段.max - step + 1; end = 新段.max
    return next++

关键优化是双缓冲:当前段用到 10%~20% 时异步预取下一段,避免段用尽瞬间的取号延迟尖刺。服务重启会浪费未用完的号段,属于可接受代价。号段模式在多种开源分布式 ID 组件中都有实现(部分组件同时提供雪花与号段两种模式),具体行为以对应项目官方文档为准。

方案五:Redis 自增

利用 Redis 单线程的原子自增命令:

INCR order:id            # 输出:1001
INCRBY order:id 1000     # 输出:11001,一次领一段可减少网络往返

优点:命令简单、原子、性能高,天然全局递增。缺点:强依赖 Redis,需考虑持久化与故障切换;必须接受 ID 不连续——未开持久化时故障重启可能从更早的值继续甚至回退,因此生产上推荐用 INCRBY 批量取号并开启合适的持久化策略。

方案对比与选型

方案趋势递增性能依赖组件适用场景
数据库自增严格递增数据库单库、量小的内部系统
数据库步长趋势递增数据库少量实例的中小系统
UUID无序极高非主键的唯一标识
雪花算法趋势递增极高高并发、分库分表主键
号段模式趋势递增数据库需要好看且有序的业务 ID
Redis 自增严格递增Redis已有 Redis 且能接受跳号

选型建议:

  1. 单库小系统:直接用数据库自增,不要过度设计;
  2. 分库分表 + 高并发:雪花算法(无中心依赖)或号段模式(对外 ID 更连续);
  3. 对外展示订单号:号段模式或"日期 + 号段"组合,避免被推算业务量;
  4. 已有 Redis 且能接受跳号INCRBY;需要不可猜测的标识用 UUID,但仅作业务唯一键。

常见坑

  • 机器位分配冲突:手工配置 workerId 极易撞号,应由配置中心统一分配;
  • 时钟回拨未处理:这是雪花算法唯一但致命的缺陷,必须显式处理并告警;
  • 前端精度丢失:雪花 ID 超出 JavaScript 安全整数范围(2^53-1),接口层应序列化为字符串返回;
  • 号段浪费:服务频繁重启会浪费大量号段,步长不宜设置过大。

小结:分布式 ID 按"是否依赖中心组件"分两类——雪花算法与 UUID 本地生成、性能最高,其中雪花趋势递增适合做主键;数据库自增、号段模式与 Redis 自增依赖中心组件,但 ID 更连续易读。选型时优先考虑趋势递增与不可猜测两项要求,务必处理时钟回拨,并在接口层把长整型 ID 转为字符串返回前端。

笔记加载中…