
分布式 ID 生成方案从数据库自增到雪花算法目录自增 ID 的局限UUID数据库号段Redis INCR雪花算法时钟回拨方案对比小结自增 ID 的局限在很多系统刚开始建设的时候ID 并不是一个需要特别关注的问题。创建一张表设置一个自增主键数据库负责保证每条数据都有唯一编号这种方式简单、稳定而且几乎没有额外成本。但随着业务规模扩大系统从单体逐渐演变成多实例部署甚至开始进行分库分表后原本简单的自增 ID 就遇到了问题多个服务节点如何保证生成的 ID 仍然全局唯一单库场景下所有数据写入都经过同一个数据库自增序列天然是连续且唯一的。但到了分布式环境每个数据库节点都有自己的自增序列它们并不知道其他节点已经生成了哪些 ID。例如数据库A用户表分片1 数据库B用户表分片2 id1 张三 id1 李四 id2 王五 id2 赵六对于数据库 A 来说ID1 是合法的对于数据库 B 来说ID1 同样也是合法的。但放到整个系统来看两个 ID 已经发生冲突。有人可能会想到提前给不同节点划分 ID 范围比如让 A 使用奇数B 使用偶数或者给每个数据库设置不同的起始值和增长步长。但这种方式随着节点数量变化会越来越难维护。新增机器、数据迁移、业务扩容都可能导致原有规则失效。因此在分布式系统中ID 的生成不能再依赖某一个数据库实例的自增能力而需要设计一套独立的 ID 生成机制让不同节点在没有互相协调的情况下也能生成全局唯一的编号。UUIDUUID 最大的优点是简单。应用节点之间不需要协调本地直接生成就能用StringidUUID.randomUUID().toString();// 550e8400-e29b-41d4-a716-446655440000零依赖、无网络开销、理论上不会重复。但简单也意味着牺牲了一些数据库场景下比较重要的特性。做数据库主键性能差。InnoDB 的主键索引本质是一棵 B 树数据通常按照主键顺序插入。ID 不断递增时新数据基本追加到索引尾部而随机 UUID 会让数据插入位置更加分散增加 B 树页分裂和索引维护成本。加上 UUID 是 36 个字符的字符串比 8 字节的 Long 整数大好几倍索引占用空间也更大。可读性差。出了问题排查时user_id1001比user_id550e8400-e29b-41d4-a716-446655440000好认得多。UUID 适合做请求追踪 ID、日志关联 ID 这类不需要排序、不做主键的场景。拿来做数据库主键代价不小。数据库号段一种自然的思路是引入一个专门的 ID 服务由它统一分配编号。但如果每次生成 ID 都访问数据库本质上只是把压力从业务表转移到了发号表。号段模式解决的就是这个问题一次取一批用完了再取下一批。数据库只在号段用完时才被访问一次平时 ID 生成全在内存里完成。实际实现中通常用两个号段交替当前号段快用完时提前加载下一个号段避免用完时的数据库查询阻塞请求。美团的 Leaf 就是这个思路用双 Buffer 保证在任何时刻都有可用的号段。号段模式的 ID 有序递增对数据库索引友好也不依赖时钟。缺点是强依赖数据库号段表挂了就发不了号双 Buffer 和预加载可以缓解但不能完全消除。Redis INCRRedis 的INCR命令是原子操作天然适合做分布式 ID 生成publiclonggenerateId(Stringkey){returnredisTemplate.opsForValue().increment(key);}Redis 单线程模型保证了原子性不需要加锁高并发下也不会重复。实现比号段模式简单得多不需要维护号段加载逻辑。但它有自己的问题。号段模式一次数据库交互拿一批 ID之后全在内存里生成。Redis INCR 每次生成 ID 都要一次网络往返QPS 很高时网络开销会成为瓶颈。另外 Redis 宕机了 ID 就发不出来了虽然可以做主从切换但切换过程中有短暂的不可用窗口。还有 Redis 的持久化策略 —— 如果配置不当故障恢复后计数器可能回退导致 ID 重复需要额外保证持久化策略的正确性。Redis INCR 适合对性能要求不是极端高、已有 Redis 基础设施的场景。雪花算法Snowflake 是 Twitter 开源的分布式 ID 生成方案。它不依赖任何外部存储本地直接生成性能极高。核心思想是把一个 64 位的 Long 整数拆成几段每段有不同的含义41 位时间戳提供了约 69 年的使用周期对于绝大多数业务系统已经足够。12 位序列号意味着同一毫秒同一机器最多生成 4096 个 ID。5 位数据中心 5 位机器 ID最多支持 1024 个节点。生成逻辑取当前时间戳如果和上一次相同序列号自增如果不同序列号归零。序列号溢出时等下一毫秒再生成。nextId(): timestamp 当前时间戳 if timestamp lastTimestamp: 时钟回拨抛异常或等待恢复 if timestamp lastTimestamp: sequence if sequence maxSequence: 等待下一毫秒重新获取 timestamp else: sequence 0 lastTimestamp timestamp return (timestamp - epoch) 22 | datacenterId 17 | workerId 12 | sequence位运算的拆解 22把时间戳移到最高位 17和 12分别给数据中心和机器 ID 留出位置最后 12 位留给序列号。OR 操作把各段拼成一个完整的 64 位整数。雪花算法纯本地生成没有网络开销ID 整体递增对数据库索引友好。缺点是对系统时钟有依赖。时钟回拨雪花算法最大的隐患是时钟回拨。线上环境里更常见的是机器时间同步异常。例如某台机器因为时间同步配置异常时钟突然回拨几秒雪花算法会立即拒绝生成 ID最终表现为业务请求失败。正常情况下服务器时间是单调递增的但 NTP 同步、闰秒调整、虚拟机迁移这些场景都可能导致时间往回跳。处理方式通常有两种小幅回拨等待恢复。检测到回拨幅度在几毫秒以内时sleep 一小段时间再重试。大部分 NTP 同步造成的小幅回拨都能扛过去。if(timestamplastTimestamp){longoffsetlastTimestamp-timestamp;if(offset5){Thread.sleep(offset1);// 等待回拨恢复timestampSystem.currentTimeMillis();if(timestamplastTimestamp){thrownewRuntimeException(时钟回拨过大拒绝生成ID);}}else{thrownewRuntimeException(时钟回拨过大拒绝生成ID);}}大幅回拨直接报警。回拨几秒以上通常意味着运维问题这时候比兜底更有意义的是报警让运维介入排查根因。也有方案在 64 位中预留几位做回拨计数器每次检测到回拨就加一这样即使时间回退也不会重复。代价是留给时间戳或序列号的位数变少了。方案对比方案性能趋势递增适合作为主键运维成本适合场景数据库自增高是适合低单机部署数据量不大UUID高否一般低追踪 ID、日志关联数据库号段高是适合中业务主键已有数据库Redis INCR高是适合中已有 Redis 基础设施雪花算法极高是适合低大规模分布式系统小结分布式 ID 的设计是在业务规模和系统复杂度之间做取舍。对于早期系统数据库自增通常已经足够当业务发展到分库分表阶段可以考虑号段模式或者 Redis INCR如果需要更高并发、更大规模的分布式生成能力再引入雪花算法。很多时候问题并不是现有方案性能不够而是在业务还没有复杂到那个程度时就提前引入了复杂的分布式方案最终增加了系统维护成本。