本文目录导读:

这是一个非常经典且重要的分布式系统问题,在分布式系统中,传统的数据库自增ID(如MySQL的 AUTO_INCREMENT)不再适用,因为无法保证在多个数据库节点、高并发下的全局唯一性和趋势递增。
下面我将系统性地介绍几种最主流的 Java 分布式 ID 实现方案,包括其原理、优缺点、Java代码示例(或核心逻辑),以及适用场景。
核心需求回顾
一个好的分布式ID生成器通常需要满足:
- 全局唯一性:这是最基本的要求。
- 趋势递增:对于数据库索引(特别是InnoDB的B+树索引)友好,利于插入性能。
- 高可用、低延迟:在分布式环境下能稳定快速生成。
- 高QPS:支持每秒数万甚至数百万的并发请求。
基于数据库的号段模式(Leaf-Segment 方案)
这是最“经典”且广泛使用的方式,它不是每次生成ID都查询数据库,而是批量获取一个ID号段,然后在应用内存中分配,极大降低了数据库压力。
原理:
- 数据库表:维护一个表,记录每个业务的
max_id(当前最大ID)和step(步长,如1000)。 - 批量获取:应用启动或当前号段快用完时,去数据库执行
UPDATE ... SET max_id = max_id + step,然后获取新的[新max_id - step + 1, 新max_id]号段。 - 内存分配:应用服务将这段ID缓存在本地内存(如
AtomicLong),每次请求自增1。 - 双Buffer优化:为了平滑和防抖,可以维护两个Buffer(segment),当第一个Buffer消耗到50%时,异步去加载下一个Buffer,这样即使数据库短暂抖动,也不会影响ID生成。
优点:
- 趋势递增:生成的ID是递增的,对MySQL的聚簇索引非常友好。
- 高并发:大部分操作在应用内存中完成,性能极高。
- 无状态:应用服务本身不存储状态,方便水平扩展。
缺点:
- 依赖数据库:虽然压力很小,但数据库仍是弱点,如果数据库宕机,服务可能会在预加载的号段耗尽后不可用(由于双Buffer优化,有较大的缓冲期)。
- ID有“空洞”:如果服务重启,已经申请但未消耗完的号段会被丢弃,导致ID不连续(但这是可以接受的)。
Java 核心逻辑示例:
import java.util.concurrent.atomic.AtomicLong;
public class IdGenerator {
// 假设已经通过数据库或配置中心拿到了这个号段
private long maxId = 1000; // 当前批次的最大ID
private long minId = 1; // 当前批次的最小ID
private AtomicLong currentId = new AtomicLong(minId);
// 实际项目中,这里应该有异步线程从DB加载下一个号段
public synchronized long nextId() {
long id = currentId.getAndIncrement();
if (id > maxId) {
// 当前号段耗尽,需要去数据库加载下一个号段
// loadNextSegmentFromDB();
throw new RuntimeException("No more ID in current segment, need to load next.");
}
return id;
}
}
建议:生产中推荐直接使用开源实现,如 美团 Leaf 或 百度 UidGenerator,它们都实现了双Buffer和号段模式,非常成熟稳定。
雪花算法(Snowflake)
这也是极流行的方案,它的核心思想是:将一个64位的Long整数拆分成不同的部分,使其在全局范围内唯一且趋势递增。
经典 64 位结构:
| 1 bit (符号位) | 41 bit (时间戳) | 10 bit (工作机器ID) | 12 bit (序列号) |
|---|---|---|---|
| 始终为0 | 毫秒级时间戳(相对值) | 机器标识(可组合为机房+机器) | 同一毫秒内的自增序号 |
原理:
- 唯一性:通过
时间戳+机器ID+序列号组合保证。 - 趋势递增:ID随时间增长,因此具有趋势递增性。
优点:
- 高并发:纯内存操作,单机QPS可达数百万(取决于
sequence位数)。 - 无网络依赖:不依赖数据库或Redis。
- 全局唯一:在时钟不回溯且有唯一机器ID分配的情况下,绝对唯一。
- 趋势递增。
缺点:
- 依赖机器时钟:如果系统时钟发生回拨(如NTP同步),可能会产生重复ID,这是一个重大隐患,通常需要额外的机制来防止(如等待或报警)。
- ID长度长:Long类型64位,对于某些前端或数据库(如JavaScript的
Number.MAX_SAFE_INTEGER= 53位)需要以字符串形式传输。
Java 核心代码示例:
public class SnowflakeIdWorker {
private final long twepoch = 1288834974657L; // 起始时间戳,可以自定义
private final long workerIdBits = 10L; // 机器ID的位数
private final long sequenceBits = 12L; // 序列号位数
private final long workerIdShift = sequenceBits;
private final long timestampLeftShift = sequenceBits + workerIdBits;
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
private long workerId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdWorker(long workerId) {
if (workerId < 0 || workerId > (-1L ^ (-1L << workerIdBits))) {
throw new IllegalArgumentException("Worker ID 超出范围");
}
this.workerId = workerId;
}
public synchronized long nextId() {
long timestamp = timeGen();
// 处理时钟回拨问题(简化处理:等待)
if (timestamp < lastTimestamp) {
long offset = lastTimestamp - timestamp;
if (offset <= 5) {
try {
wait(offset * 2); // 等待时钟追上
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨,拒绝生成ID");
}
} else {
throw new RuntimeException("时钟回拨过多,拒绝生成ID");
}
}
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
// 当前毫秒内的序号耗尽,等待下一毫秒
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - twepoch) << timestampLeftShift) |
(workerId << workerIdShift) |
sequence;
}
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
private long timeGen() {
return System.currentTimeMillis();
}
}
建议:如果采用此方案,务必做好的机器ID(WorkerID)的分配(如基于ZooKeeper、数据库或配置中心),并实现时钟回拨的容错机制,很多框架也提供了“容忍时间差”或“使用NTP的平滑同步”策略。
| 特性 | 数据库号段模式 | 雪花算法 | Redis INCR | UUID |
|---|---|---|---|---|
| 唯一性 | 强 | 强(需防时钟回拨) | 强 | 极强 |
| 趋势递增 | ✅ 是 | ✅ 是 | ✅ 是 | ❌ 否(无序) |
| 性能 (单机QPS) | 高(内存分配) | 非常高(纯内存、无IO) | 高(但受限于网络IO) | 最高 |
| 依赖 | 数据库(弱依赖) | 无 | Redis(强依赖) | 无 |
| ID 长度 | 64位 (Long) | 64位 (Long) | 64位 (Long) | 128位 (String) |
| 实现复杂度 | 中等 | 中等(需解决时钟回拨) | 低 | 极低 |
| 典型应用 | 电商订单ID、核心业务表主键 | 微服务、日志、消息 | 小型系统、非核心业务 | 非索引字段、日志ID |
真实企业级方案选型建议
-
对于核心业务(如订单、支付、用户ID):
- 首选:美团 Leaf(基于号段模式)或 百度 UidGenerator(基于雪花算法,但利用数据库预分配WorkerID来解决启动问题),它们经过了大规模生产环境验证,成熟稳定。
- 原因:对数据库依赖可控,可水平扩展,ID有序。
-
对于高并发、无依赖的场景(如日志、全链路追踪ID):
- 首选:改造成改良版雪花算法,将10位机器ID拆成
5位进程ID+5位数字自增,或者使用ZooKeeper进行全局机器ID分配。 - 原因:性能最高,不依赖外部组件。
- 首选:改造成改良版雪花算法,将10位机器ID拆成
-
对于中小型项目或临时场景:
- 直接使用:Redis INCR,简单、可靠,或者使用数据库主键自增 + Redis 的组合。
- 最简单的实现:
Redis INCR或UUID。 - 最实用的实现:号段模式(批量获取,内存分配)。
- 最高性能的实现:雪花算法(纯内存,无IO)。
对于Java开发,建议直接采用 Leaf (美团) 或 UidGenerator (百度) 等成熟框架,它们在工厂中已经解决了分布式ID生成的大部分痛点,能够直接集成到你的项目中。
如果你需要具体的代码实现或集成示例,可以告诉我你采用的框架(如Spring Boot),我可以给你更详细的配置和代码。