分布式ID方案选型与实战:雪花算法、号段、UUID 怎么选?

0 阅读6分钟

前言

分库分表、多节点写入时,单机自增ID不够用了。订单号、用户ID、日志追踪ID都要全局唯一、尽量有序、高性能,还要考虑数据库压力和安全。市面上方案很多:UUID、雪花算法、号段模式、Redis 自增……到底选哪个?

本文从场景出发,对比常见分布式ID方案的特点和适用场景,并给出 Java/Go 下的实战示例,方便你在项目里直接选型、落地。


1. 先搞清楚:分布式ID要满足什么?

要求说明
全局唯一多节点、多表不能重复
趋势递增利于 MySQL 主键索引、分库分表路由
高性能本地生成、少依赖外部服务
高可用不依赖单点,故障影响小
信息安全尽量不暴露业务量、不连续可猜

不同业务侧重点不同:订单号要可读、防篡改;用户ID要短、可暴露;链路追踪要全局唯一、易生成。


2. 方案一:UUID

2.1 特点

  • 优点:本地生成、无网络、实现简单、绝对不重复。
  • 缺点:无序(随机 UUID)、36 位字符串、做 MySQL 主键会导致页分裂、索引效率差;无业务含义。

2.2 适用场景

  • 临时凭证、一次性 token、日志追踪 ID。
  • 不做主键、不按范围查询的场景。

2.3 示例

// Java
import java.util.UUID;
String id = UUID.randomUUID().toString();  // 550e8400-e29b-41d4-a716-446655440000
// Go
import "github.com/google/uuid"
id := uuid.New().String()
# 命令行
uuidgen

2.4 小结

适合「只要唯一、不关心顺序」的场景,不适合做 MySQL 主键或订单号。


3. 方案二:数据库自增(单库/多库步长)

3.1 单库自增

单库时用 AUTO_INCREMENT 即可,简单可靠。分库分表后各自自增会重复,不能直接当全局ID。

3.2 多库步长自增

每台库设置不同起点和步长,例如 2 台库:

  • 库1:auto_increment_increment=2auto_increment_offset=1 → 1, 3, 5, 7…
  • 库2:auto_increment_increment=2auto_increment_offset=2 → 2, 4, 6, 8…

优点:实现简单、趋势递增。
缺点:扩容要调步长、迁移麻烦;强依赖数据库,写压力大。

适合节点数固定、写入量不大的场景,现在用得越来越少。


4. 方案三:号段模式(Segment)

4.1 原理

由中心服务(或数据库表)一次分配一段 ID(例如 1~1000),应用在内存里自增使用,用完了再取下一段。

[DB/服务] 分配 1~1000 → 应用A 本地 1,2,3...1000
         分配 1001~2000 → 应用B 本地 1001,1002...

4.2 优点

  • 数据库压力小(一次取一批)。
  • ID 连续、趋势递增,适合做主键、分库分表。
  • 可做双 buffer 预取,几乎无阻塞。

4.3 缺点

  • 依赖中心服务或 DB;中心挂了要等恢复或提前多取几段。
  • 会有「段内连续、段间可能浪费」的步长,一般可接受。

4.4 数据库表设计示例

CREATE TABLE segment_id (
    biz_tag VARCHAR(32) PRIMARY KEY COMMENT '业务类型:order/user',
    max_id   BIGINT     NOT NULL DEFAULT 0 COMMENT '当前最大ID',
    step     INT        NOT NULL DEFAULT 1000 COMMENT '号段长度',
    update_time DATETIME NOT NULL
);

-- 取号段(原子更新)
UPDATE segment_id SET max_id = max_id + step, update_time = NOW()
WHERE biz_tag = 'order';
-- 然后 SELECT max_id, step 得到 [max_id - step + 1, max_id]

4.5 Java 取号段示例(简化)

// 伪代码:取号段
public synchronized long nextId(String bizTag) {
    if (current >= end) {
        // 从 DB 拉取新号段
        Segment seg = segmentDao.getNextSegment(bizTag);
        current = seg.getMaxId() - seg.getStep();
        end = seg.getMaxId();
    }
    return ++current;
}

4.6 小结

适合中等 QPS、希望少打 DB、且能接受依赖中心的业务,很多大厂内部 ID 服务就是这样做的。


5. 方案四:雪花算法(Snowflake)

5.1 结构(64 bit)

  • 1 bit:符号位,固定 0。
  • 41 bit:毫秒级时间戳,可用约 69 年。
  • 10 bit:机器/节点 ID(最多 1024 个节点)。
  • 12 bit:同一毫秒内序列(每毫秒最多 4096 个 ID)。

同一毫秒内超过 4096 可等待下一毫秒或扩展序列位。

5.2 优点

  • 本地生成、无网络、高性能。
  • 趋势递增,对 MySQL 主键和 B+ 树友好。
  • 可包含时间信息,便于排查和粗略排序。

5.3 缺点

  • 强依赖时钟:时钟回拨会导致重复或异常,需要做时钟回拨处理(等待/报错/换节点)。
  • 机器 ID 要分配好,否则多节点可能重复。

5.4 Java 示例(简化版)

public class SnowflakeIdGenerator {
    private final long workerId;
    private final long epoch = 1609459200000L; // 2021-01-01 00:00:00
    private long lastTimestamp = -1L;
    private long sequence = 0L;
    private static final long SEQUENCE_BITS = 12L;
    private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS); // 4095

    public synchronized long nextId() {
        long now = System.currentTimeMillis();
        if (now < lastTimestamp) {
            throw new IllegalStateException("时钟回拨,拒绝生成ID");
        }
        if (now == lastTimestamp) {
            sequence = (sequence + 1) & MAX_SEQUENCE;
            if (sequence == 0) {
                now = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }
        lastTimestamp = now;
        return ((now - epoch) << 22) | (workerId << 12) | sequence;
    }

    private long tilNextMillis(long last) {
        long now = System.currentTimeMillis();
        while (now <= last) now = System.currentTimeMillis();
        return now;
    }
}

5.5 Go 示例(第三方库)

import "github.com/bwmarrin/snowflake"

node, _ := snowflake.NewNode(1)  // 节点ID 1
id := node.Generate()            // 返回 int64

5.6 小结

适合高 QPS、多节点、能保证时钟可靠的场景,是当前最常用的分布式ID方案之一。生产务必处理时钟回拨(NTP、虚拟机挂起等)。


6. 方案五:Redis INCR

6.1 用法

INCR id:order
# 返回 1, 2, 3...

可加前缀做业务隔离:id:orderid:user

6.2 优点

  • 实现简单、性能高。
  • 可设置过期或按日期 key,方便按天清零等策略。

6.3 缺点

  • 依赖 Redis 可用性;持久化不当可能丢一段(AOF 可缓解)。
  • 纯自增,无时间信息;要做成趋势递增需要 key 设计(如带日期)。

适合对绝对连续要求不高、已有 Redis 的辅助ID(如活动计数、短链ID)。


7. 方案对比与选型建议

方案唯一性有序性性能依赖适用场景
UUID临时ID、追踪ID、不做主键
库步长DB节点数固定、小规模
号段中心/DB中高QPS、可接受中心依赖
雪花趋势递增时钟+节点ID高QPS、多节点、主键/订单
RedisRedis辅助ID、计数、短链

简单选型

  • 订单号、用户ID、主键:优先 雪花号段;不能接受时钟风险选号段。
  • 临时 token、链路 IDUUID 即可。
  • 已有 Redis、非主键:可用 Redis INCR 做补充。

8. 实战注意点

8.1 订单号可读性

雪花是数字,可直接用;若要带日期、业务前缀,可在前面拼字符串,例如:ORD202502141234567890(日期 + 雪花后几位或完整雪花转码)。

8.2 号段与雪花混用

  • 主键、分库分表路由:用雪花或号段。
  • 对外展示订单号:可在雪花前加前缀、或单独用号段/Redis 生成短号。

8.3 安全与防爬

  • 不要用连续自增对外暴露;雪花或号段对外可做一次编码(如 Base62、洗牌算法)。
  • 敏感接口加签名、限流,不单靠 ID 防刷。

9. 总结

  • UUID:无顺序要求、不做主键时用。
  • 号段:少打 DB、趋势递增、可接受中心服务时用。
  • 雪花:高 QPS、多节点、主键/订单号首选,注意时钟回拨与节点ID分配。
  • Redis:辅助ID、计数、短链等补充方案。

按业务选一种为主、其他补充,就能覆盖绝大多数后端分布式ID需求。