如何生成分布式唯一ID?

wen IT资讯 238

如何生成分布式唯一ID? | 从原理到实战的完整指南

目录导读

  1. 为什么需要分布式唯一ID?
  2. 分布式ID核心需求与挑战
  3. 六大主流方案详解
  4. 方案对比与选型建议
  5. 常见问题QA

为什么需要分布式唯一ID?

在单体应用时代,使用数据库自增主键(如MySQL的AUTO_INCREMENT)足以应付所有ID生成需求,但进入分布式、微服务架构后,系统被拆分为多个独立服务,每个服务都有自己的数据库,此时面临三个核心问题:

如何生成分布式唯一ID?

  • 分库分表场景下,无法保证全局唯一自增
  • 高并发写入时,数据库自增会成为性能瓶颈
  • 多个服务需要生成跨服务可用的唯一标识(如订单号、用户ID)

案例: 电商系统双11大促,订单服务每秒需生成10万+订单号,若用自增ID,数据库单点写入压力巨大,且分库后不同库的ID会重复。


分布式ID核心需求与挑战

一个好的分布式唯一ID方案必须满足:

需求 说明 反例
全局唯一 整个分布式系统内无重复 UUID重复概率极低但非零
趋势递增 对数据库B+树索引友好 完全无序的ID会导致页分裂
高可用 生成服务不能因单点故障停止 依赖单一Redis的incr命令
高吞吐 支持10万+QPS生成 数据库自增值产生瓶颈
业务含义 可反解析时间戳、机器信息 UUID无任何业务含义

六大主流方案详解

UUID(通用唯一识别码)

原理: 基于时间戳、MAC地址、随机数生成128位标识。

// Java示例
String id = UUID.randomUUID().toString(); // 形如:f47ac10b-58cc-4372-a567-0e02b2c3d479

优点:

  • 实现极度简单,纯本地生成
  • 性能极高,无网络开销

致命缺点:

  • 完全无序:无法保证趋势递增,作为数据库主键会导致频繁页分裂(InnoDB B+树需要大量重排)
  • 长度长:36字符含横杠,占用存储空间
  • 无业务含义:不可解析时间或机器

适用场景:

  • 非数据库主键的临时标识(如日志ID)
  • 强调绝对唯一但不要求有序的场景

数据库自增ID/号段模式

原理1(自增): 利用数据库auto_increment特性。

原理2(号段): 每次从数据库取一个号段(如1000个ID),缓存到应用内存,用完再取下一段。

// 号段模式核心伪代码
public class IdGenerator {
    private long currentMaxId; // 当前可用最大ID
    private long step;         // 号段步长
    private long nextId;       // 当前可用ID
    public synchronized long nextId() {
        if (nextId > currentMaxId) {
            // 向数据库申请新号段,并更新currentMaxId
            // UPDATE id_alloc SET max_id = max_id + step WHERE biz_tag = 'order'
            // 乐观锁防止并发冲突
            this.currentMaxId = maxIdFromDB + step;
            this.nextId = maxIdFromDB + 1;
        }
        return nextId++;
    }
}

优点:

  • 严格递增,对数据库索引友好
  • 可带业务含义(如不同业务分配不同号段)

缺点:

  • 号段用完时存在数据库瓶颈(需更新行记录)
  • 依赖数据库可用性,宕机则生成停止

改进: 使用双号段预缓存(美团Leaf方案),提前缓存下一个号段避免断层。


Redis INCR/INCRBY

原理: 利用Redis的原子自增操作。

# 每次执行产生唯一递增ID
INCR order_id
# 带业务前缀
INCR order:id:202405

优点:

  • 性能极高(10万+QPS)
  • 天然原子性,无重复

缺点:

  • 依赖Redis持久化:若启用RDB快照+AOF,重启可能数据丢失导致ID重复
  • 受Redis单机性能限制(可集群但增加复杂度)
  • 每生成一个ID需一次网络RTT

改进: 使用Lua脚本批量生成连续ID,减少网络开销。


雪花算法(Snowflake,最经典方案)

原理: 生成一个64位long类型ID,结构如下:

0  | 41位时间戳  | 10位机器ID | 12位序列号
符号位(1bit)    毫秒级时间   workerID   0~4095自旋

算法要点:

  • 41位时间戳:使用当前时间与自定义纪元(如2010年11月4日)的差值,可支持约69年
  • 10位机器ID:支持1024个节点(5位数据中心+5位机器)
  • 12位序列号:同一毫秒内可生成4096个不同ID,若耗尽则等待下一毫秒

Java核心实现(简化版):

public class SnowflakeIdWorker {
    private long workerId;
    private long sequence = 0L;
    private long lastTimestamp = -1L;
    public synchronized long nextId() {
        long timestamp = timeGen();
        if (timestamp < lastTimestamp) {
            throw new RuntimeException("时钟回拨异常");
        }
        if (timestamp == lastTimestamp) {
            sequence = (sequence + 1) & 4095;
            if (sequence == 0) timestamp = tilNextMillis(lastTimestamp);
        } else {
            sequence = 0L;
        }
        lastTimestamp = timestamp;
        return ((timestamp - EPOCH) << 22) | (workerId << 12) | sequence;
    }
}

优点:

  • 趋势递增、高性能(纯本地运算,无网络)
  • 可反解析时间戳和机器信息
  • 支持高并发(单毫秒4096个)

痛点:

  • 时钟回拨问题:若服务器NTP时钟回拨,可能生成重复ID
  • 机器ID需集中分配或自动选举

典型改进:

  • 百度UidGenerator:依赖数据库+预取序列号
  • 美团Leaf:Snowflake+号段双模式

美团Leaf(组合方案)

双模式支持:

  • Leaf-Snowflake:基于雪花算法,使用ZooKeeper/Etcd自动分配workId,并记录上次时间戳到本地文件,若发现时钟回拨等待抛出异常
  • Leaf-Segment:号段模式+双buffer预取(提前缓存下个号段到内存)

设计亮点:

  • 时钟回拨时等待时间同步完成
  • 数据库号段使用乐观锁解决并发冲突

百度UidGenerator(增强版雪花)

原理:

  • 基于Snowflake但使用RingBuffer预生成(类似双buffer)
  • workId通过数据库的自增ID自动分配
  • 支持秒级时间戳(节省位数),序列号使用滚动自增

特点:

  • 解决时钟回拨:使用时间戳+滚动缓冲区
  • 单机QPS可达30万+

方案对比与选型建议

方案 唯一性 有序性 可用性 性能 适用场景
UUID 极低概率重复 完全无序 极高 极高 非主键临时标识
数据库自增 严格唯一 严格递增 小系统、单库
号段模式 严格唯一 趋势递增 业务需连续ID
Redis INCR 严格唯一 严格递增 高并发但可接受Redis依赖
雪花算法 依赖时间+机器 趋势递增 极高 默认主流方案
美团Leaf 严格唯一 趋势递增 极高 金融级、互联网大厂首选
百度UidGenerator 严格唯一 趋势递增 极高 极致性能场景

选型三步法:

  1. 简单需求:若集群节点<100,可接受小概率时钟回拨 → 裸用雪花算法
  2. 中等需求:要求高可用、无回拨 → 使用Leaf-Snowflake(依赖ZK)
  3. 大厂生产:必须永不重复+毫秒级故障恢复 → 使用Leaf-Segment双buffer

常见问题QA

Q1:雪花算法时钟回拨了怎么办?
A:有三种处理策略:

  • 等待:阻塞直到时钟回正(适合回拨小于1秒)
  • 抛异常:人工介入修复(严格场景不可接受)
  • 备用序列号:使用高位序列号段跳过回拨时间段(美团Leaf法)

Q2:如何自动分配机器ID(workId)?
A:常用两种方式:

  • ZooKeeper/Etcd临时节点,启动时注册,获取唯一id
  • 数据库自增:每次启动插入一行获取自增ID,但重启需注意复用策略

Q3:Redis做ID生成如何防止数据丢失?
A:必须开启AOF持久化(everysec模式)并配合RDB快照,但极端断电仍可能重复,建议雪花算法优先,Redis方案用于非关键业务。

Q4:不同业务如何生成带前缀的ID?
A:数据库号段方案天然支持biz_tag列区分;雪花算法可通过调整bit位分配(如用8位做业务类型),但会减少可用的机器或序列号位数。

Q5:生成ID的最佳长度是多少?
A:

  • 数据库主键建议64位long(如雪花算法)
  • 可读性需求用字符串(如订单号:202405081234000001)
  • 绝不用UUID做主键,除非是NoSQL无关索引的场景

生成分布式唯一ID没有银弹:

  • 追求极致有序选号段模式
  • 追求高性能和简单选雪花算法并做好回拨防护
  • 追求高可用且无时钟依赖选数据库号段+双buffer

实际生产中,推荐优先考虑 美团Leaf 或基于其原理的自研实现,它集合了雪花和号段的优点,对于大多数小型微服务团队,Snowflake + 手动配置workId已经足够解决99%的问题。

抱歉,评论功能暂时关闭!