分布式 ID

分布式系统中,数据库自增主键仅能保证单库唯一,跨库分表场景必须使用全局唯一 ID 生成方案。核心要求:全局唯一、趋势递增(减少 B+ 树分裂)、高性能、高可用。


Snowflake 算法

Twitter 开源,64 bit 整型,结构如下:

 0        | 41 bit        | 10 bit        | 12 bit
符号位(0)  | 毫秒时间戳     | 机器 ID        | 序列号
          | 约 69 年      | 1024 台机器    | 每毫秒 4096 个

纯 Java 实现

public class SnowflakeIdGenerator {
    private static final long EPOCH          = 1700000000000L; // 自定义纪元
    private static final long WORKER_BITS    = 5L;
    private static final long DATACENTER_BITS= 5L;
    private static final long SEQUENCE_BITS  = 12L;
 
    private static final long MAX_WORKER_ID      = ~(-1L << WORKER_BITS);      // 31
    private static final long MAX_DATACENTER_ID  = ~(-1L << DATACENTER_BITS);  // 31
    private static final long MAX_SEQUENCE        = ~(-1L << SEQUENCE_BITS);    // 4095
 
    private static final long WORKER_SHIFT     = SEQUENCE_BITS;                       // 12
    private static final long DATACENTER_SHIFT = SEQUENCE_BITS + WORKER_BITS;         // 17
    private static final long TIMESTAMP_SHIFT  = SEQUENCE_BITS + WORKER_BITS + DATACENTER_BITS; // 22
 
    private final long workerId;
    private final long datacenterId;
    private long sequence    = 0L;
    private long lastStamp   = -1L;
 
    public SnowflakeIdGenerator(long workerId, long datacenterId) {
        if (workerId > MAX_WORKER_ID || workerId < 0)
            throw new IllegalArgumentException("workerId out of range");
        if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0)
            throw new IllegalArgumentException("datacenterId out of range");
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }
 
    public synchronized long nextId() {
        long now = System.currentTimeMillis();
 
        if (now < lastStamp) throw new RuntimeException("时钟回拨,拒绝生成 ID");
 
        if (now == lastStamp) {
            sequence = (sequence + 1) & MAX_SEQUENCE;
            if (sequence == 0) now = waitNextMillis(lastStamp); // 同毫秒序号耗尽,等待下一毫秒
        } else {
            sequence = 0L;
        }
        lastStamp = now;
 
        return ((now - EPOCH) << TIMESTAMP_SHIFT)
             | (datacenterId    << DATACENTER_SHIFT)
             | (workerId        << WORKER_SHIFT)
             | sequence;
    }
 
    private long waitNextMillis(long lastStamp) {
        long ms = System.currentTimeMillis();
        while (ms <= lastStamp) ms = System.currentTimeMillis();
        return ms;
    }
}

问题:依赖本地时钟,时钟回拨会导致 ID 重复。


美团 Leaf

Leaf 提供两种模式,可同时开启,按场景选用。

Leaf-Segment(数据库号段模式)

数据库每次批量预取一个号段(如 1000 个 ID),用完再取,减少数据库压力。双 Buffer 机制:当前号段剩余 10% 时异步加载下一号段,实现无缝切换。

CREATE TABLE `leaf_alloc` (
  `biz_tag`     VARCHAR(128) NOT NULL,
  `max_id`      BIGINT       NOT NULL DEFAULT 1,
  `step`        INT          NOT NULL COMMENT '步长,每次分配的号段大小',
  `description` VARCHAR(256),
  `update_time` TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`biz_tag`)
) ENGINE=InnoDB;
 
INSERT INTO leaf_alloc(biz_tag, max_id, step, description)
VALUES ('order', 1, 2000, '订单 ID');
leaf:
  name: order-service
  segment:
    enable: true
    url: jdbc:mysql://localhost:3306/leaf_db
    username: root
    password: root

优点:ID 全局趋势递增,可读性强,与 DB 同步频率低;缺点:强依赖 DB,号段耗尽瞬间有短暂延迟。

Leaf-Snowflake(Snowflake 模式)

使用 ZooKeeper 分配 workerID(解决 Snowflake 机器 ID 手动配置问题),启动时向 ZK 注册,本地缓存 workerID。

leaf:
  name: order-service
  snowflake:
    enable: true
    zk-address: 127.0.0.1:2181
    port: 8090
@Autowired
private SegmentService segmentService;
 
@Autowired
private SnowflakeService snowflakeService;
 
// 号段模式
long id = segmentService.getId("order");
 
// Snowflake 模式
long id = snowflakeService.getId();

百度 uid-generator

基于 Snowflake,Worker ID 通过 DB 分配(启动时插入一行,以行 ID 作为 workerID),支持时钟回拨(拒绝发放 + 等待修复)。提供 CachedUidGenerator 预生成 ID 填充 RingBuffer,异步取用,吞吐极高。


各方案对比

方案依赖趋势递增时钟回拨QPS适用场景
数据库自增MySQL无影响单库、低并发
Snowflake是(毫秒级)有风险极高独立部署、无 ZK
Leaf-SegmentMySQL是(号段内单调)无影响通用业务 ID
Leaf-SnowflakeZooKeeper有风险极高已有 ZK 基础设施
uid-generatorMySQL有处理极高高吞吐场景
Redis INCRRedis无影响简单场景,需持久化

Redis INCR 方案

简单场景可用 Redis INCRBY 批量获取号段:

// 每次从 Redis 批量拿 1000 个 ID
long maxId = redisTemplate.opsForValue().increment("order:id:seq", 1000);
long minId = maxId - 1000 + 1;
// 本地使用 [minId, maxId] 区间的 ID

注意:Redis 持久化配置不当时,重启可能回退 ID,需配合 AOF。


相关链接

  • ShardingSphere — 分库分表场景下通常需要分布式 ID
  • Zookeeper — Leaf-Snowflake / uid-generator 的 WorkerID 注册中心
  • 分布式 ← 返回分布式目录