做了几年后端,分布式ID这话题几乎绕不开。订单号、消息ID、日志追踪ID、优惠券编号,只要系统一拆成多节点,一个全局唯一的ID生成方案就得提上日程。标题看着简单,真选型的时候坑却不少:有人图省事直接用UUID,结果索引膨胀到怀疑人生;有人抄了个雪花算法,时钟一回调就冒出重复ID;还有人拿Redis当万能药,持久化策略没想清楚就上了线,重启之后直接蒙圈。所以我想把这几年在分布式ID上踩过的坑和用过的方案整理一遍,给正在做分布式架构、微服务改造,或者面试前想系统梳理这块知识的同学一份能直接参考的清单。
1. 为什么分布式系统里ID成了一个问题
1.1 单机自增ID的风光与局限
单机时代,一把 auto_increment 就能解决所有问题。MySQL 的自增主键简单可靠、天生有序,还不用业务代码操心,新插入一行拿到一个不重复的整数就可以了。但在分布式架构下,这个方案立刻变得尴尬。假设订单表拆成了 16 个库,每个库都从 1 开始自增,第一单库1生成订单号1,第一单库2也生成订单号1,到了汇总层一看,两个订单号长得一模一样,完全没法定位。
有人会说,那就给每个库设置不同的初始值和步长。比如双库场景,库1 从 1 开始每次加 2,库2 从 2 开始每次加 2,这样两个库的ID看似不冲突。但问题在于扩容:你原本只有 2 个库,后来业务量上来要扩成 4 个,就必须重新调整每个库的初始值和步长,而且已经生成过的ID没法回退。线上执行这种变更,谁做谁知道,改错一步就是主键冲突和脏数据横飞。所以 "数据库自增ID + 分库分表" 只能作为过渡方案,不能当长期分布式ID方案用。
1.2 分布式ID的基本要求
判断一个分布式ID方案好不好,不能只看"全局唯一"这一个指标,生产环境里通常还要满足几个硬性条件。
首先是全局唯一,不用展开。其次是趋势递增或单调递增。为什么强调趋势递增?因为很多系统用ID做主键,而MySQL InnoDB的聚簇索引是按主键顺序物理排列的。如果ID完全随机,插入时B+树节点会频繁页分裂,产生大量随机IO,写入性能直线下降。反过来,ID趋势递增时,新记录基本追加在末尾,写入效率高得多。第三是高可用低延迟,ID生成服务不能成为瓶颈,一旦它挂了,整个写入链路就全部卡死。第四是能够承载一定的业务语义,比如从ID里能读出生成时间、机器信息,方便排查问题。最后看安全合规,对外暴露的ID不能有明显的规律性,否则别人能通过订单号推测你的订单量,甚至使用遍历方式爬数据。
这些要求往往互相冲突。趋势递增和不可猜测就不太好同时满足,UUID完全随机但不可猜测,雪花ID趋势递增但容易被猜测。因此实际落地时,没有银弹,只有"根据业务场景做取舍"。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 常见的分布式ID生成方案横向对比
2.1 UUID/ULID:最简单但别乱用
UUID是很多新手的首选,无他,代码里一行UUID.randomUUID().toString()就完事,本地生成,不需要任何网络请求,性能无限大。但把UUID直接塞进数据库做主键,往往是最糟糕的体验。UUID是128位,转成字符串有36个字符,存储空间比bigint大得多;再加上它完全无序,作为InnoDB主键时索引分裂严重,数据量一旦上千万,写入性能肉眼可见地下降。
如果一定要用UUID,可以做个折中:把UUID二进制化,存成BINARY(16),省掉横杠和字符编码开销;或者给UUID额外加一个雪花ID作为业务主键,把UUID降级为业务流水号、追溯ID。像分布式追踪里的traceId,通常用UUID或Ulid就够,因为它们不需要参与数据库主键排序,只承担链路关联职责。ULID比UUID强的地方在于它带48位时间戳,有序且能按时间排序,但长度并未缩小多少,适合对可排序有要求但不需要短ID的场景。
2.2 数据库自增与号段模式
数据库自增虽然是单机时代的产物,但分布式场景下它升级成了"号段模式",目前仍在大量中间件中使用。号段模式的核心思路是:与其每次生成ID都访问数据库,不如一次从数据库取一批ID放内存里慢慢用,用完了再去取一批。
具体实现不复杂。数据库里建一张发号表,字段大致是biz_tag(业务标识)、max_id(当前已发放的最大ID)、step(步长)、version(乐观锁版本号)。每次取号时执行类似下面的SQL:
sql复制UPDATE id_allocator
SET max_id = max_id + step, versi
