分布式数据库的全局序列生成,核心难题就是在多个节点同时写入时,如何保证生成的ID全局唯一、有序可控、且性能不拖垮整个系统。最常见的解决方案有三类:基于时间戳的雪花算法(Snowflake)、基于号段模式的数据库预分配机制、以及基于Redis或ZooKeeper等中间件的集中式协调方案。每种方案都有明确的适用场景和取舍,不存在一刀切的完美答案,关键是根据你的业务并发量、ID长度要求、是否需要有序性来选型。
为什么分布式环境下ID生成这么难?
单机数据库用自增主键很简单,数据库内部一个计数器递增就行。但一旦数据分片到多个节点,每个节点各自维护自增序列,必然产生重复ID。如果用UUID,虽然全局唯一,但128位太长、无序、无法索引,对MySQL的InnoDB聚簇索引性能是灾难性的。所以业界一直在寻找一种既能保证全局唯一、又尽量有序、长度可控、性能高的ID生成策略。这就是全局序列生成问题的本质。
方案一:雪花算法(Snowflake)及其变体
雪花算法是目前最主流的分布式ID生成方案。它把一个64位的Long型整数拆分成几个字段:1位符号位、41位时间戳、10位机器ID(5位数据中心ID + 5位工作节点ID)、12位序列号。同一毫秒内,同一节点可以生成4096个不同ID,理论上每毫秒每个节点的吞吐量是4096,全局并发能力非常强。
public class SnowflakeIdGenerator {
private final long workerId;
private final long datacenterId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(long workerId, long datacenterId) {
if (workerId > 31 || workerId < 0) throw new IllegalArgumentException("workerId out of range");
if (datacenterId > 31 || datacenterId < 0) throw new IllegalArgumentException("datacenterId out of range");
this.workerId = workerId;
this.datacenterId = datacenterId;
}
public synchronized long nextId() {
long currentTimestamp = System.currentTimeMillis();
if (currentTimestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards");
}
if (currentTimestamp == lastTimestamp) {
sequence = (sequence + 1) & 4095;
if (sequence == 0) {
currentTimestamp = waitNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = currentTimestamp;
return ((currentTimestamp - 1288834974657L) << 22)
| (datacenterId << 17)
| (workerId << 12)
| sequence;
}
private long waitNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}
雪花算法的核心风险是时钟回拨。如果服务器NTP同步导致时间倒退,可能生成重复ID。解决办法有三种:一是等待时钟追上来再继续生成;二是在时间戳字段中预留几位作为逻辑时钟,遇到回拨时逻辑时钟递增;三是直接拒绝服务并告警。百度的UidGenerator、美团的Leaf都是在雪花算法基础上做了改进。
方案二:号段模式(Segment模式)
号段模式的思路是:不每次生成一个ID,而是一次性从数据库或中间件申请一段ID范围(比如1000个),缓存在本地内存中慢慢用。用完了再申请下一段。这样大幅减少了对中心节点的请求压力,同时天然避免了时钟回拨问题。
// 号段模式核心逻辑示意
public class SegmentIdGenerator {
private AtomicLong currentId = new AtomicLong(0);
private long maxId = 0;
private long step = 1000;
public synchronized long nextId() {
if (currentId.get() >= maxId) {
// 从数据库加载新号段
loadNewSegment();
}
return currentId.incrementAndGet();
}
private void loadNewSegment() {
// SELECT update_segment('id_gen', step) FROM dual;
// 实际实现需要数据库层面的原子更新
// UPDATE segment_table SET max_id = max_id + step WHERE biz_type = 'order'
// 然后读取新的max_id赋值给maxId
}
}
号段模式的优点是性能极高、实现简单、不依赖时钟。缺点是多节点间的ID不是严格连续的(每个节点拿到的号段之间有间隙),而且如果某个节点崩溃,它缓存的号段没用完就浪费了。美团Leaf的号段模式通过双Buffer优化(当前号段用到10%时异步加载下一段)解决了性能抖动问题。
方案三:基于Redis或ZooKeeper的集中式协调
如果你的系统已经有Redis集群或ZooKeeper集群,可以直接利用它们的原子递增能力来生成全局唯一ID。Redis的INCR命令是单线程原子操作,天然保证唯一性。ZooKeeper可以利用其znode的顺序特性生成有序ID。
// Redis INCR 方式
public long generateIdByRedis(String key) {
return redisTemplate.opsForValue().increment(key);
}
// 或者用Redis的INCRBY配合时间戳前缀
public String generateOrderId() {
long seq = redisTemplate.opsForValue().increment("order:seq:" + LocalDate.now());
return String.format("%s%06d", LocalDate.now().toString().replace("-", ""), seq);
}
这种方案的优势是实现最简单,不需要引入额外的ID生成服务。但瓶颈很明显:所有ID生成请求都要经过Redis或ZooKeeper,高并发下中心节点压力巨大,而且引入了网络往返延迟。通常只适合中小规模系统,或者作为兜底方案。
唯一性冲突的检测与兜底策略
无论用哪种方案,在极端情况下都可能出现冲突。比如雪花算法的时钟回拨没处理好、号段模式的数据库更新出现脏读、Redis主从切换导致计数丢失。所以生产环境必须有兜底机制。
第一层兜底是数据库层面的唯一索引约束。即使应用层生成了重复ID,数据库插入时会报唯一键冲突错误,应用捕获后重试即可。第二层是在ID生成服务中加入冲突检测逻辑,比如生成ID后先查一次是否已存在(适合低并发场景)。第三层是引入全局冲突表,记录所有冲突事件用于监控和告警。
有序性与性能的权衡
很多业务场景要求ID是递增的,比如订单号、流水号。严格递增在分布式环境下代价很高,因为需要全局协调。实际做法通常是"趋势递增":在同一个节点内严格递增,不同节点之间通过时间戳或节点ID前缀来保证整体趋势有序。比如把节点ID放在高位,这样同一节点生成的ID天然大于其他节点早期生成的ID。
如果业务不需要严格有序,只需要唯一,那选择面就宽很多,UUID、随机数、哈希都可以。但对于MySQL InnoDB这种以聚簇索引为核心的存储引擎,无序插入会导致大量页分裂,写入性能可能下降30%以上。所以大多数金融、电商系统还是倾向于使用趋势递增的ID。
实际选型建议
日均订单量在百万级以下的系统,用数据库号段模式或Redis INCR就够了,简单可靠。日均千万级以上的高并发系统,推荐雪花算法或其改进版(如百度UidGenerator),配合时钟回拨处理和双Buffer优化。如果已经有成熟的中间件基础设施,Leaf、Tinyid、UidGenerator这些开源方案可以直接集成,避免重复造轮子。
另外要注意ID的位数设计。64位Long型是主流,转换成十进制最多19位,足够用几十年。如果需要更短的ID(比如15位以内),可以考虑自定义编码方案,但要预留足够的机器位和序列位,否则扩容时会出问题。
总结
分布式数据库的全局序列生成,本质上是在唯一性、有序性、性能、可用性四个维度之间做取舍。没有银弹,只有最适合你业务场景的方案。核心原则是:先明确业务对ID的硬性要求,再评估系统规模和基础设施,最后选择成熟方案并做好冲突兜底。把这三步走扎实,ID生成就不会成为系统瓶颈。
