系统设计:分布式 ID 生成器

分布式 ID 生成器设计详解:从数据库自增到 Snowflake、号段模式、Leaf 等方案的演进、对比与实战选型,解决唯一性、趋势递增、高性能与高可用四大核心挑战。

系统设计:分布式 ID 生成器

在分布式系统中,如何高效、可靠地生成全局唯一且趋势递增的 ID?


背景与需求

为什么不能用数据库自增 ID?

在单机时代,MySQL 的 auto_increment 完全够用。但在分布式系统中,自增 ID 面临几个问题:

  1. 单点瓶颈:所有插入都依赖单台数据库,无法水平扩展
  2. 数据迁移困难:分库分表后,各库自增 ID 可能冲突
  3. 信息暴露:自增 ID 会暴露业务规模(如订单总量)
  4. 非趋势递增:分库场景下 ID 不连续,对 B+ 树不友好

核心需求

需求说明优先级
全局唯一任何时刻、任何节点生成的 ID 都不重复P0
趋势递增大致按时间递增,对索引友好P1
高性能支持每秒百万级生成P1
高可用不存在单点故障P1
可扩展方便增加节点,不影响已有服务P2
信息安全ID 不暴露业务信息P2

方案演进

方案一:UUID

import uuid

# UUID v4:完全随机
uid = uuid.uuid4()  # e.g., 'f47ac10b-58cc-4372-a567-0e02b2c3d479'

# UUID v1:基于时间戳 + MAC 地址
uid1 = uuid.uuid1()

优点:

  • 本地生成,无网络依赖,性能极高
  • 全局唯一,理论冲突概率趋近于 0

缺点:

  • 无序:完全随机,对数据库索引极不友好(页分裂严重)
  • 过长:36 个字符(含横线),存储和传输成本高
  • 信息可读性差:无法从 ID 中推断生成时间或来源

适用场景:日志 trace_id、文件名等不需要排序的场景。


方案二:数据库分段(号段模式)

核心思想:一次性从数据库申请一个号段(如 1-1000),用完再申请,减少数据库访问次数。

-- 号段表
create table id_segment (
    biz_tag varchar(50) primary key,
    max_id bigint not null,
    step int not null default 1000,
    version int not null default 0
);

-- 初始化
insert into id_segment values ('order', 0, 1000, 0);

获取号段的逻辑:

import threading

class SegmentIdGenerator:
    def __init__(self, db, biz_tag, step=1000):
        self.db = db
        self.biz_tag = biz_tag
        self.step = step
        self.current_id = 0
        self.max_id = 0
        self.lock = threading.Lock()

    def get_id(self):
        with self.lock:
            if self.current_id >= self.max_id:
                self.load_next_segment()
            self.current_id += 1
            return self.current_id

    def load_next_segment(self):
        while True:
            row = self.db.query(
                "SELECT max_id, version FROM id_segment WHERE biz_tag = %s",
                self.biz_tag
            )
            old_max, version = row['max_id'], row['version']
            new_max = old_max + self.step

            affected = self.db.execute(
                "UPDATE id_segment SET max_id = %s, version = version + 1 "
                "WHERE biz_tag = %s AND version = %s",
                new_max, self.biz_tag, version
            )
            if affected == 1:
                self.current_id = old_max
                self.max_id = new_max
                break

优点:

  • 趋势递增(segments 之间连续)
  • 生成性能高(内存中分配,QPS 可达数万)
  • 数据库压力小(一次分配 1000 个号)

缺点:

  • 仍存在数据库单点(可主从+哨兵缓解)
  • 号段用完时的切换瞬间可能出现延迟尖刺

方案三:Snowflake(雪花算法)

Twitter 开源的分布式 ID 生成方案,是业界最广泛使用的方案。

结构(64 位)

| 符号位 | 时间戳(41位) | 数据中心 ID(5位) | 机器 ID(5位) | 序列号(12位) |
|--------|----------------|--------------------|----------------|----------------|
| 1 bit  | 41 bits        | 5 bits             | 5 bits         | 12 bits        |
| 0      | 毫秒级时间戳   | 0-31               | 0-31           | 0-4095         |
  • 时间戳位:约可使用 69 年(从自定义的纪元开始)
  • 数据中心 + 机器:共 10 位,支持 1024 个节点
  • 序列号:每毫秒每节点最多生成 4096 个 ID,理论 QPS = 4096 * 1000 ≈ 400 万

Python 实现

import time
import threading

class Snowflake:
    def __init__(self, datacenter_id: int, worker_id: int, epoch=1609459200000):
        self.epoch = epoch
        self.datacenter_id = datacenter_id & 0x1F
        self.worker_id = worker_id & 0x1F
        self.sequence = 0
        self.last_timestamp = -1
        self.lock = threading.Lock()

        self.worker_id_bits = 5
        self.datacenter_id_bits = 5
        self.sequence_bits = 12

        self.worker_id_shift = self.sequence_bits
        self.datacenter_id_shift = self.sequence_bits + self.worker_id_bits
        self.timestamp_shift = (self.sequence_bits + self.worker_id_bits +
                                self.datacenter_id_bits)
        self.sequence_mask = (1 << self.sequence_bits) - 1

    def _current_time_ms(self) -> int:
        return int(time.time() * 1000)

    def _wait_next_millis(self, last_ms: int) -> int:
        ms = self._current_time_ms()
        while ms <= last_ms:
            ms = self._current_time_ms()
        return ms

    def next_id(self) -> int:
        with self.lock:
            timestamp = self._current_time_ms()

            if timestamp < self.last_timestamp:
                raise Exception("Clock moved backwards")

            if timestamp == self.last_timestamp:
                self.sequence = (self.sequence + 1) & self.sequence_mask
                if self.sequence == 0:
                    timestamp = self._wait_next_millis(self.last_timestamp)
            else:
                self.sequence = 0

            self.last_timestamp = timestamp

            return ((timestamp - self.epoch) << self.timestamp_shift |
                    self.datacenter_id << self.datacenter_id_shift |
                    self.worker_id << self.worker_id_shift |
                    self.sequence)

Snowflake 的关键问题

1. 时钟回拨

如果服务器时间被 NTP 同步回调,last_timestamp > current_timestamp,会导致 ID 重复。

解决方案:

  • 等待:阻塞到时间追上(适合小幅度回拨)
  • 异常抛出:大幅度回拨时直接报错,人工介入
  • 美团的 Leaf:采用「双 buffer」机制,时钟回拨时切换到备用号段

2. 机器 ID 分配

需要保证每个节点的 datacenter_id + worker_id 唯一。

解决方案:

  • Zookeeper 分配:启动时向 ZK 注册,获取唯一 worker ID
  • 配置文件:小规模部署直接手动配置
  • 容器化:通过环境变量注入

3. JavaScript 兼容

JS 的 Number 最大安全整数是 2^53-1,Snowflake 的 64 位可能溢出。

解决方案:返回时将 ID 转为字符串,或缩短时间戳位数。


方案四:美团的 Leaf(号段 + Snowflake 双模式)

美团开源的分布式 ID 生成服务。

Leaf-segment(号段模式优化版)

  • 双 buffer 机制:当前号段用到 90% 时,异步加载下一个号段
  • 数据库高可用:多机房部署,容灾自动切换
  • 趋势递增:ID 单调递增,适合数据库主键

Leaf-snowflake(雪花算法优化版)

  • 使用 Zookeeper 持久顺序节点分配 worker_id
  • 弱依赖 ZK:ZK 挂掉不影响已有节点发号
  • 解决时钟回拨:启动时向 ZK 上报时间戳

方案对比

方案趋势递增性能高可用长度信息暴露适用场景
UUID v4❌⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐36 字符无Trace ID、文件名
号段模式✅⭐⭐⭐⭐⭐⭐⭐64bit弱数据库主键
Snowflake✅⭐⭐⭐⭐⭐⭐⭐⭐⭐64bit可推时间高并发、时间排序
Leaf✅⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐64bit弱大规模、企业级

面试答题框架

第一步:明确需求(30秒)

确认是否需要趋势递增、QPS 要求、高可用级别。

第二步:分析方案(1分钟)

列举 UUID、号段模式、Snowflake、Leaf 四种方案的优缺点。

第三步:深入核心方案(5分钟)

画 Snowflake 的 64 位结构图,讲解时钟回拨和 worker ID 分配。

第四步:总结 trade-off(1分钟)

强趋势递增选号段模式,极高性能选 Snowflake,企业级选 Leaf。


常见问题

Q:Snowflake 的 4096/毫秒够用吗?

单机单业务线通常足够(400 万 QPS)。不够时增加序列号位数或部署更多 worker。

Q:为什么不用 Redis 的 INCR?

单点 + 网络 RTT,性能不如本地生成。

Q:分库分表场景下 ID 设计要注意什么?

用全局唯一 ID(Snowflake/Leaf)作为逻辑主键,shard key 用独立业务字段。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页