在分布式系统中,一个看似简单却至关重要的问题就是如何生成全局唯一的 ID。无论是订单系统、用户系统还是消息队列,都离不开唯一标识符。今天,我就带大家一起深入了解分布式 ID 发号器的设计思路和实现方案。
为什么我们需要分布式 ID 发号器?
先来看看传统单体应用中,我们通常怎么生成 ID:
在单体应用中,我们习惯用数据库自增 ID,简单直接。但分布式系统中,这种方式就会遇到瓶颈:
- 单点问题:所有 ID 生成都依赖于一个数据库,一旦数据库挂了,整个系统就无法生成新 ID
- 性能瓶颈:高并发场景下,数据库成为性能瓶颈
- 水平扩展困难:多个数据库实例可能生成重复 ID
所以,我们需要一个专门的分布式 ID 发号器。
分布式 ID 应该具备什么特性?
一个好的分布式 ID 应该具备以下特性:
- 全局唯一性:在整个分布式系统中,ID 不能重复
- 高性能:生成速度要快,支持高并发
- 高可用:发号器不能成为系统的单点故障
- 趋势递增:方便数据库建立索引和分区
- 信息安全:ID 中不应该包含敏感业务信息
- 长度适中:过长的 ID 会增加存储和传输成本
想象一下,如果你正在开发一个电商平台,每天需要处理百万级的订单。每个订单都需要一个唯一 ID,这个 ID 不仅要保证全局唯一,还要能快速生成,否则用户下单时可能会感到延迟。
常见的分布式 ID 生成方案
1. UUID/GUID
UUID 是最简单的实现方式,直接调用 JDK 自带的 API 就能生成:
java
import java.util.UUID;
public class UUIDGenerator {
public static String generateId() {
return UUID.randomUUID().toString().replace("-", "");
}
public static void main(String[] args) {
System.out.println(generateId()); // 输出:a7c85fada7a04b1e92e4c2123a02ae5b
}
}
优点:
- 生成简单,无需依赖外部系统
- 生成速度快
- 零集中式风险
缺点:
- 32 个字符太长
- 完全随机,不是递增
- 数据库索引效率低
2. 数据库自增 ID
这是最传统的方式,利用 MySQL 的 auto_increment 特性:
我们可以通过单独的数据表来实现:
sql
CREATE TABLE `id_generator` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`business_type` varchar(32) NOT NULL COMMENT '业务类型',
`created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_business_type` (`business_type`)
);
Java 代码:
java
public class DbIdGenerator {
private final DataSource dataSource;
public DbIdGenerator(DataSource dataSource) {
this.dataSource = dataSource;
}
public long generateId(String businessType) {
String insertSql = "INSERT INTO id_generator(business_type) VALUES(?) ON DUPLICATE KEY UPDATE id=LAST_INSERT_ID(id+1)";
try (Connection conn = dataSource.getConnection();
PreparedStatement pstmt = conn.prepareStatement(insertSql, Statement.RETURN_GENERATED_KEYS)) {
pstmt.setString(1, businessType);
pstmt.executeUpdate();
try (ResultSet rs = pstmt.getGeneratedKeys()) {
if (rs.next()) {
return rs.getLong(1);
}
throw new RuntimeException("Failed to generate id");
}
} catch (SQLException e) {
throw new RuntimeException("Error generating id", e);
}
}
}
优点:
- 递增,实现简单
- ID 长度合适
- 对应用透明
缺点:
- 依赖数据库,存在单点风险
- 性能受限于数据库写入速度
- 水平扩展困难
3. 号段模式
为了解决频繁访问数据库的问题,我们可以一次性获取一批 ID,用完再取:

实现代码:
java
public class SegmentIdGenerator {
private final DataSource dataSource;
private final Map<String, IdSegment> segmentMap = new ConcurrentHashMap<>();
private final int step = 1000; // 每次获取1000个ID
public SegmentIdGenerator(DataSource dataSource) {
this.dataSource = dataSource;
}
public synchronized long generateId(String bizType) {
IdSegment segment = segmentMap.get(bizType);
// 如果号段不存在或已用完,申请新号段
if (segment == null || segment.getIdle() <= 0) {
segment = applyNewSegment(bizType);
segmentMap.put(bizType, segment);
}
return segment.nextId();
}
private IdSegment applyNewSegment(String bizType) {
// 注意:以下是MySQL兼容的实现方式
try (Connection conn = dataSource.getConnection()) {
// 1. 更新max_id
String updateSql = "UPDATE id_generator SET max_id = max_id + ? WHERE business_type = ?";
try (PreparedStatement pstmt = conn.prepareStatement(updateSql)) {
pstmt.setInt(1, step);
pstmt.setString(2, bizType);
int rows = pstmt.executeUpdate();
if (rows == 0) {
// 如果业务类型不存在,则插入
String insertSql = "INSERT INTO id_generator(business_type, max_id) VALUES(?, ?)";
try (PreparedStatement insertStmt = conn.prepareStatement(insertSql)) {
insertStmt.setString(1, bizType);
insertStmt.setInt(2, step);
insertStmt.executeUpdate();
}
return new IdSegment(1, step);
}
}
// 2. 查询当前max_id
String querySql = "SELECT max_id FROM id_generator WHERE business_type = ?";
try (PreparedStatement pstmt = conn.prepareStatement(querySql)) {
pstmt.setString(1, bizType);
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
long maxId = rs.getLong("max_id");
long minId = maxId - step + 1;
return new IdSegment(minId, maxId);
}
}
}
throw new RuntimeException("Failed to apply id segment");
} catch (SQLException e) {
throw new RuntimeException("Error applying id segment", e);
}
}
private static class IdSegment {
private final long minId;
private final long maxId;
private long currentId;
public IdSegment(long minId, long maxId) {
this.minId = minId;
this.maxId = maxId;
this.currentId = minId - 1;
}
public synchronized long nextId() {
if (currentId < maxId) {
return ++currentId;
}
throw new RuntimeException("Segment has no idle id");
}
public long getIdle() {
return maxId - currentId;
}
}
}
优点:
- 大幅减少数据库访问频率
- 保持了 ID 的递增性
- 相比单个 ID 申请,性能提升明显
缺点:
- 仍然依赖数据库
- 应用重启会浪费一部分 ID
- 多实例部署时 ID 不是严格递增的
ID 浪费优化方案:
- 通过持久化当前号段信息到本地文件/Redis,应用重启时加载恢复
- 使用小号段策略,平衡 ID 利用率和数据库访问频率
- 双 buffer 机制,保持一个可用号段,同时异步加载下一号段
java
// 号段持久化示例
private void persistSegment(String bizType, IdSegment segment) {
File file = new File("segments/" + bizType + ".json");
file.getParentFile().mkdirs();
try (FileWriter writer = new FileWriter(file)) {
// 写入号段信息
String json = "{\"minId\":" + segment.minId +
",\"maxId\":" + segment.maxId +
",\"currentId\":" + segment.currentId + "}";
writer.write(json);
} catch (IOException e) {
log.warn("Failed to persist segment for {}", bizType, e);
}
}
// 应用启动时恢复号段
private void loadPersistedSegments() {
File dir = new File("segments");
if (!dir.exists() || !dir.isDirectory()) {
return;
}
for (File file : dir.listFiles()) {
if (file.isFile() && file.getName().endsWith(".json")) {
String bizType = file.getName().substring(0, file.getName().length() - 5);
try {
// 解析JSON文件恢复号段
// 省略具体实现...
segmentMap.put(bizType, recoveredSegment);
} catch (Exception e) {
log.warn("Failed to load segment for {}", bizType, e);
}
}
}
}
4. Redis 实现
Redis 的原子操作也可以用来生成 ID:
java
public class RedisIdGenerator {
private final StringRedisTemplate redisTemplate;
public RedisIdGenerator(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
public long generateId(String bizType) {
String key = "id:generator:" + bizType;
Long id = redisTemplate.opsForValue().increment(key);
if (id == null) {
throw new RuntimeException("Failed to generate id from Redis");
}
return id;
}
}
优点:
- 性能高
- 实现简单
- ID 单调递增
缺点:
- 依赖 Redis 的可用性
- 若 Redis 服务宕机需保证快速 failover
- 集群分片环境下需要解决 ID 的连续性问题
5. 雪花算法(Snowflake)
雪花算法是 Twitter 开源的分布式 ID 生成算法,生成的 ID 由 64 位二进制组成:
- 第 1 位:最高位固定为 0,预留扩展
- 第 2-42 位:41 位时间戳,精确到毫秒
- 第 43-52 位:10 位工作机器 ID(5 位数据中心+5 位机器 ID)
- 第 53-64 位:12 位序列号,每毫秒可产生 4096 个 ID
Java 实现:
java
public class SnowflakeIdGenerator {
// 起始时间戳:2023-01-01 00:00:00
private final long startEpoch = 1672502400000L;
// 位数分配
private final long dataCenterIdBits = 5L;
private final long workerIdBits = 5L;
private final long sequenceBits = 12L;
// 最大值
private final long maxWorkerId = ~(-1L << workerIdBits);
private final long maxDataCenterId = ~(-1L << dataCenterIdBits);
// 偏移量
private final long workerIdShift = sequenceBits;
private final long dataCenterIdShift = sequenceBits + workerIdBits;
private final long timestampShift = sequenceBits + workerIdBits + dataCenterIdBits;
// 序列掩码
private final long sequenceMask = ~(-1L << sequenceBits);
private long workerId;
private long dataCenterId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(long workerId, long dataCenterId) {
// 参数检查
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("Worker ID can't be greater than %d or less than 0", maxWorkerId));
}
if (dataCenterId > maxDataCenterId || dataCenterId < 0) {
throw new IllegalArgumentException(String.format("DataCenter ID can't be greater than %d or less than 0", maxDataCenterId));
}
this.workerId = workerId;
this.dataCenterId = dataCenterId;
}
public synchronized long nextId() {
long timestamp = timeGen();
// 时钟回拨检查
if (timestamp < lastTimestamp) {
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
// 如果是同一毫秒生成的,则递增序列号
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
// 同一毫秒内序列号用完
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
// 不同毫秒内,序列号重置为0
sequence = 0L;
}
lastTimestamp = timestamp;
// 组装ID
return ((timestamp - startEpoch) << timestampShift) |
(dataCenterId << dataCenterIdShift) |
(workerId << workerIdShift) |
sequence;
}
// 阻塞到下一个毫秒
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
// 获取当前时间戳
private long timeGen() {
return System.currentTimeMillis();
}
public static void main(String[] args) {
SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator(1, 1);
for (int i = 0; i < 10; i++) {
System.out.println(idGenerator.nextId());
}
}
}
优点:
- 高性能,无网络请求
- ID 趋势递增,有利于索引
- 可以包含时间、机器等信息
- 不依赖外部系统
缺点:
- 依赖系统时钟,时钟回拨会导致 ID 重复
- 工作机器 ID 需要全局唯一分配
- 起始时间设置会影响 ID 使用寿命
时间位溢出问题: 雪花算法使用 41 位表示毫秒级时间戳,以 2023-01-01 为起始时间计算,可用约 69 年。当接近溢出时,有两种处理方案:
- 提前调整起始时间 :在时间位即将用尽前,更新应用中的
startEpoch
值,确保新生成的 ID 不会与历史 ID 冲突 - 位移降级方案:保留高位时间戳不变,将低位序列号和工作机器位减少,扩展时间位
java
// 时间位扩展示例(43位时间戳+8位机器ID+12位序列号)
public void extendTimeBits() {
// 原配置
long oldTimestampBits = 41L;
long oldWorkerIdBits = 10L; // 数据中心(5)+工作机器(5)
// 新配置:扩展时间位,缩小机器ID位
long newTimestampBits = 43L;
long newWorkerIdBits = 8L;
// 仅修改偏移量计算和ID组装逻辑
this.timestampShift = sequenceBits + newWorkerIdBits;
// ID组装时使用新的位移逻辑
// ...
}
6. 百度的 UidGenerator
基于雪花算法的优化版本:
UidGenerator 使用 28 位表示秒级时间戳,从特定起始时间(默认 2016-05-20)计算,理论上可支持约 8.7 年。时间戳用尽后,可通过调整起始时间来扩展使用期限。
UidGenerator 提供了两种实现:
- DefaultUidGenerator:适用于单机部署
- CachedUidGenerator:适用于高并发场景,使用 RingBuffer 提前生成 ID
关键代码示例:
java
public class UidGenerator {
// 起始时间戳:2016-05-20(UidGenerator默认值)
private final long epoch = 1463673600L; // 单位:秒
private final long timeBits = 28L;
private final long workerBits = 22L;
private final long sequenceBits = 13L;
private final long maxWorkerId = ~(-1L << workerBits);
private final long maxSequence = ~(-1L << sequenceBits);
private final long workerIdShift = sequenceBits;
private final long timestampShift = workerBits + sequenceBits;
private long workerId;
private long sequence = 0L;
private long lastSecond = -1L;
public UidGenerator(long workerId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("Worker ID can't be greater than %d or less than 0", maxWorkerId));
}
this.workerId = workerId;
}
public synchronized long nextId() {
long currentSecond = getCurrentSecond();
// 时钟回拨检查
if (currentSecond < lastSecond) {
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d seconds", lastSecond - currentSecond));
}
// 如果是同一秒内
if (currentSecond == lastSecond) {
sequence = (sequence + 1) & maxSequence;
// 同一秒内序列号用完
if (sequence == 0) {
currentSecond = waitForNextSecond(lastSecond);
}
} else {
// 不同秒内,序列号重置为0
sequence = 0L;
}
lastSecond = currentSecond;
// 组装ID
return ((currentSecond - epoch) << timestampShift) |
(workerId << workerIdShift) |
sequence;
}
private long waitForNextSecond(long lastTimestamp) {
long timestamp = getCurrentSecond();
while (timestamp <= lastTimestamp) {
timestamp = getCurrentSecond();
}
return timestamp;
}
private long getCurrentSecond() {
return System.currentTimeMillis() / 1000L;
}
}
优点:
- 使用秒级时间戳,ID 长度更短
- 采用 RingBuffer 预先生成 ID,性能更高
- 支持自动获取 workerId,简化部署
缺点:
- 算法相对复杂
- 依赖系统时钟
- 秒级时间戳会降低 ID 的时间精度
7. 美团的 Leaf
Leaf 结合了号段模式和雪花算法的优点,提供了两种模式:
- 号段模式(Leaf-segment):从数据库批量获取 ID
- 雪花模式(Leaf-snowflake):基于雪花算法,解决了时钟回拨问题
对于时钟回拨,Leaf 采用了以下策略:
备用 workerId 策略: Leaf 预先分配两个 workerId,主备 ID 在同一数据中心内,但属于不同 ID 段。例如:
- 主 workerId:0-511 区间(前 9 位)
- 备用 workerId:512-1023 区间(前 9 位置 1)
当检测到大幅时钟回拨时,自动切换到备用 workerId 继续生成 ID,确保新生成的 ID 仍然唯一且符合雪花算法格式。切换后,即使与历史 ID 的时间戳部分相同,由于 workerId 不同,仍能保证唯一性。
优点:
- 解决了时钟回拨问题
- 提供号段和雪花两种模式,灵活适应不同场景
- 高可用设计,支持双活机房
缺点:
- 实现较为复杂
- 需要 ZooKeeper 等组件辅助 workerId 分配
跨语言兼容性实现
分布式系统往往涉及多种编程语言,确保不同语言的 ID 生成算法互相兼容非常重要。
其他语言实现雪花算法
Go 语言实现
go
type Snowflake struct {
startEpoch int64
workerID int64
dataCenterID int64
sequence int64
workerIDBits int64
dataCenterIDBits int64
sequenceBits int64
maxWorkerID int64
maxDataCenterID int64
sequenceMask int64
workerIDShift int64
dataCenterIDShift int64
timestampShift int64
lastTimestamp int64
mutex sync.Mutex
}
func NewSnowflake(workerID, dataCenterID int64) (*Snowflake, error) {
sf := &Snowflake{
startEpoch: 1672502400000, // 2023-01-01 00:00:00
workerIDBits: 5,
dataCenterIDBits: 5,
sequenceBits: 12,
sequence: 0,
lastTimestamp: -1,
}
sf.maxWorkerID = -1 ^ (-1 << sf.workerIDBits)
sf.maxDataCenterID = -1 ^ (-1 << sf.dataCenterIDBits)
sf.sequenceMask = -1 ^ (-1 << sf.sequenceBits)
sf.workerIDShift = sf.sequenceBits
sf.dataCenterIDShift = sf.sequenceBits + sf.workerIDBits
sf.timestampShift = sf.sequenceBits + sf.workerIDBits + sf.dataCenterIDBits
if workerID > sf.maxWorkerID || workerID < 0 {
return nil, fmt.Errorf("Worker ID can't be greater than %d or less than 0", sf.maxWorkerID)
}
if dataCenterID > sf.maxDataCenterID || dataCenterID < 0 {
return nil, fmt.Errorf("DataCenter ID can't be greater than %d or less than 0", sf.maxDataCenterID)
}
sf.workerID = workerID
sf.dataCenterID = dataCenterID
return sf, nil
}
func (sf *Snowflake) NextID() (int64, error) {
sf.mutex.Lock()
defer sf.mutex.Unlock()
timestamp := time.Now().UnixNano() / 1000000 // 毫秒时间戳
if timestamp < sf.lastTimestamp {
return 0, fmt.Errorf("Clock moved backwards, refusing to generate id for %d milliseconds", sf.lastTimestamp - timestamp)
}
if timestamp == sf.lastTimestamp {
sf.sequence = (sf.sequence + 1) & sf.sequenceMask
if sf.sequence == 0 {
timestamp = sf.waitNextMillis(sf.lastTimestamp)
}
} else {
sf.sequence = 0
}
sf.lastTimestamp = timestamp
id := ((timestamp - sf.startEpoch) << sf.timestampShift) |
(sf.dataCenterID << sf.dataCenterIDShift) |
(sf.workerID << sf.workerIDShift) |
sf.sequence
return id, nil
}
func (sf *Snowflake) waitNextMillis(lastTimestamp int64) int64 {
timestamp := time.Now().UnixNano() / 1000000
for timestamp <= lastTimestamp {
timestamp = time.Now().UnixNano() / 1000000
}
return timestamp
}
Python 实现
python
import time
import threading
class Snowflake:
def __init__(self, worker_id, datacenter_id):
self.start_epoch = 1672502400000 # 2023-01-01 00:00:00
self.worker_id_bits = 5
self.datacenter_id_bits = 5
self.sequence_bits = 12
self.max_worker_id = -1 ^ (-1 << self.worker_id_bits)
self.max_datacenter_id = -1 ^ (-1 << self.datacenter_id_bits)
self.sequence_mask = -1 ^ (-1 << self.sequence_bits)
self.worker_id_shift = self.sequence_bits
self.datacenter_id_shift = self.sequence_bits + self.worker_id_bits
self.timestamp_shift = self.sequence_bits + self.worker_id_bits + self.datacenter_id_bits
if worker_id > self.max_worker_id or worker_id < 0:
raise ValueError(f"Worker ID can't be greater than {self.max_worker_id} or less than 0")
if datacenter_id > self.max_datacenter_id or datacenter_id < 0:
raise ValueError(f"Datacenter ID can't be greater than {self.max_datacenter_id} or less than 0")
self.worker_id = worker_id
self.datacenter_id = datacenter_id
self.sequence = 0
self.last_timestamp = -1
self.lock = threading.Lock()
def next_id(self):
with self.lock:
timestamp = int(time.time() * 1000)
if timestamp < self.last_timestamp:
raise Exception(f"Clock moved backwards, refusing to generate id for {self.last_timestamp - timestamp} milliseconds")
if timestamp == self.last_timestamp:
self.sequence = (self.sequence + 1) & self.sequence_mask
if self.sequence == 0:
timestamp = self._wait_next_millis(self.last_timestamp)
else:
self.sequence = 0
self.last_timestamp = timestamp
return ((timestamp - self.start_epoch) << self.timestamp_shift) | \
(self.datacenter_id << self.datacenter_id_shift) | \
(self.worker_id << self.worker_id_shift) | \
self.sequence
def _wait_next_millis(self, last_timestamp):
timestamp = int(time.time() * 1000)
while timestamp <= last_timestamp:
timestamp = int(time.time() * 1000)
return timestamp
跨语言兼容性注意事项
-
位运算一致性:
- 不同语言的位操作可能有细微差别,特别是处理有符号整数时
- Java 的 long 是有符号 64 位,Go 的 int64 和 Python 的整数处理方式不同
-
时间戳处理:
- 确保所有语言使用相同的起始时间(epochTime)
- 时间单位统一(通常为毫秒)
-
数值溢出:
- JavaScript 等语言对大整数有精度限制(最大安全整数为 53 位)
- 可考虑将 ID 转为字符串处理
-
线程安全性:
- 不同语言的同步机制差异较大
- 确保 ID 生成的原子性
云原生环境下的 ID 生成方案
在 Kubernetes 等云原生环境中,传统的基于物理机器 IP 的 WorkerId 分配方式面临挑战。
在 Kubernetes 中分配 WorkerId
- 基于 Pod 标识:
java
public class K8sWorkerIdAssigner implements WorkerIdAssigner {
@Override
public long assignWorkerId() {
try {
// 通过环境变量获取Pod信息(Kubernetes自动注入)
String podName = System.getenv("POD_NAME");
String namespace = System.getenv("POD_NAMESPACE");
if (podName == null || namespace == null) {
throw new RuntimeException("POD_NAME or POD_NAMESPACE environment variable not set");
}
// 使用命名空间和Pod名称哈希计算workerId
String uniqueId = namespace + ":" + podName;
int hashCode = uniqueId.hashCode();
// 确保在合法范围内(0-1023)
return Math.abs(hashCode) % 1024;
} catch (Exception e) {
throw new RuntimeException("Failed to assign worker id in Kubernetes", e);
}
}
}
- 使用 Kubernetes ConfigMap 分配:
java
public class ConfigMapWorkerIdAssigner implements WorkerIdAssigner {
private final KubernetesClient client;
private final String namespace;
private final String configMapName = "worker-id-allocator";
public ConfigMapWorkerIdAssigner(KubernetesClient client, String namespace) {
this.client = client;
this.namespace = namespace;
}
@Override
public long assignWorkerId() {
try {
String podName = System.getenv("POD_NAME");
// 获取或创建ConfigMap
ConfigMap configMap = client.configMaps()
.inNamespace(namespace)
.withName(configMapName)
.get();
if (configMap == null) {
configMap = new ConfigMapBuilder()
.withNewMetadata()
.withName(configMapName)
.endMetadata()
.build();
client.configMaps().inNamespace(namespace).create(configMap);
}
// 锁定ConfigMap以原子更新(使用注解作为乐观锁)
Map<String, String> data = configMap.getData();
if (data == null) {
data = new HashMap<>();
}
// 如果Pod已分配ID则复用
if (data.containsKey(podName)) {
return Long.parseLong(data.get(podName));
}
// 查找可用ID
Set<Long> usedIds = data.values().stream()
.map(Long::parseLong)
.collect(Collectors.toSet());
long workerId = 0;
while (usedIds.contains(workerId) && workerId < 1024) {
workerId++;
}
if (workerId >= 1024) {
throw new RuntimeException("No available worker id");
}
// 更新ConfigMap
data.put(podName, String.valueOf(workerId));
client.configMaps().inNamespace(namespace)
.withName(configMapName)
.edit(cm -> new ConfigMapBuilder(cm)
.withData(data)
.build());
return workerId;
} catch (Exception e) {
throw new RuntimeException("Failed to assign worker id using ConfigMap", e);
}
}
}
云环境中的时钟同步
在云环境中,虚拟机或容器的时钟可能比物理机器更容易出现偏差:
-
使用 NTP 服务:
- 在 Kubernetes 集群中部署 NTP DaemonSet
- 配置 Pod 使用集群内部 NTP 服务
-
基于心跳的时钟校准:
java// 定期向中央服务获取标准时间 @Scheduled(fixedRate = 30000) // 每30秒校准一次 public void calibrateClock() { try { ClockResponse response = timeServiceClient.getCurrentTime(); long serverTime = response.getTimestamp(); long localTime = System.currentTimeMillis(); long drift = serverTime - localTime; if (Math.abs(drift) > 50) { // 偏差超过50ms this.clockOffset = drift; // 记录偏移量 log.warn("Clock drift detected: {}ms, adjusted", drift); } } catch (Exception e) { log.error("Failed to calibrate clock", e); } } // 获取经过校准的当前时间 public long currentTimeMillis() { return System.currentTimeMillis() + clockOffset; }
混合 ID 生成方案
对于超大规模系统,单一方案往往难以满足所有需求,混合方案可以结合多种技术的优势。
号段+雪花混合模式
实现思路:
- 对于高性能场景(如订单创建),使用雪花算法本地生成 ID
- 对于低频场景(如用户注册),使用号段模式批量获取 ID
- 两种模式的 ID 在位模式上区分,如首位标识 ID 来源
java
public class HybridIdGenerator {
private static final long SEGMENT_ID_FLAG = 1L << 63; // 最高位为1表示号段ID
private static final long SNOWFLAKE_ID_FLAG = 0L; // 最高位为0表示雪花ID
private final SegmentIdGenerator segmentGenerator;
private final SnowflakeIdGenerator snowflakeGenerator;
public HybridIdGenerator(DataSource dataSource, long workerId, long dataCenterId) {
this.segmentGenerator = new SegmentIdGenerator(dataSource);
this.snowflakeGenerator = new SnowflakeIdGenerator(workerId, dataCenterId);
}
public long generateId(String bizType, boolean highPerformance) {
if (highPerformance) {
// 高性能场景使用雪花算法
long id = snowflakeGenerator.nextId();
// 确保最高位为0
return id & ~SEGMENT_ID_FLAG;
} else {
// 低频场景使用号段模式
long id = segmentGenerator.generateId(bizType);
// 设置最高位为1
return id | SEGMENT_ID_FLAG;
}
}
// 从ID解析生成方式
public boolean isSegmentId(long id) {
return (id & SEGMENT_ID_FLAG) != 0;
}
// 分解雪花ID
public SnowflakeIdInfo parseSnowflakeId(long id) {
if (isSegmentId(id)) {
throw new IllegalArgumentException("Not a snowflake ID");
}
// 解析雪花ID各部分
long timestamp = (id >> snowflakeGenerator.getTimestampShift()) + snowflakeGenerator.getStartEpoch();
long dataCenterId = (id >> snowflakeGenerator.getDataCenterIdShift()) & snowflakeGenerator.getMaxDataCenterId();
long workerId = (id >> snowflakeGenerator.getWorkerIdShift()) & snowflakeGenerator.getMaxWorkerId();
long sequence = id & snowflakeGenerator.getSequenceMask();
return new SnowflakeIdInfo(timestamp, dataCenterId, workerId, sequence);
}
// 雪花ID信息类
public static class SnowflakeIdInfo {
private final long timestamp;
private final long dataCenterId;
private final long workerId;
private final long sequence;
// 构造函数和getter省略
}
}
优点:
- 灵活适应不同场景需求
- 性能和可靠性的平衡
- 可根据 ID 前缀快速识别生成方式
缺点:
- 实现复杂度增加
- 需要额外的管理逻辑
- ID 格式不统一
区间预分配+本地生成
另一种混合模式是由中央服务器分配 ID 区间,各应用实例在区间内本地生成 ID:
java
public class RangeIdGenerator {
private final IdRangeService rangeService;
private AtomicLong currentId;
private long endId;
private final ReentrantLock lock = new ReentrantLock();
// 预加载阈值,当剩余ID数量低于此值时异步加载新区间
private final long preloadThreshold;
// 是否正在异步加载新区间
private volatile boolean isLoading = false;
public RangeIdGenerator(IdRangeService rangeService, long preloadThreshold) {
this.rangeService = rangeService;
this.preloadThreshold = preloadThreshold;
// 初始加载区间
IdRange range = rangeService.allocateRange();
this.currentId = new AtomicLong(range.getStartId() - 1);
this.endId = range.getEndId();
}
public long nextId() {
long id = currentId.incrementAndGet();
if (id <= endId) {
// 检查是否需要预加载
if (!isLoading && (endId - id) < preloadThreshold) {
asyncLoadNewRange();
}
return id;
}
// 当前区间已用尽
lock.lock();
try {
// 双重检查,避免重复加载
if (id > endId) {
IdRange newRange = rangeService.allocateRange();
currentId = new AtomicLong(newRange.getStartId());
endId = newRange.getEndId();
return currentId.get();
} else {
return id;
}
} finally {
lock.unlock();
}
}
private void asyncLoadNewRange() {
isLoading = true;
CompletableFuture.runAsync(() -> {
try {
IdRange newRange = rangeService.allocateRange();
lock.lock();
try {
// 当前区间用尽才替换
if (currentId.get() >= endId) {
currentId = new AtomicLong(newRange.getStartId() - 1);
endId = newRange.getEndId();
} else {
// 否则保存到下一个区间队列中
nextRange = newRange;
}
} finally {
lock.unlock();
}
} catch (Exception e) {
log.error("Failed to preload ID range", e);
} finally {
isLoading = false;
}
});
}
// ID区间
public static class IdRange {
private final long startId;
private final long endId;
// 构造函数和getter省略
}
}
优点:
- 大幅减少中央服务负载
- 本地生成 ID 性能高
- 支持超大规模系统
缺点:
- 区间分配需要保证原子性
- 实例宕机会浪费部分 ID
- 实现较为复杂
分布式场景下的核心挑战
在实际应用中,分布式 ID 发号器面临几个核心挑战:
-
WorkerId 分配问题
-
雪花算法:workerId 如何全局唯一分配?
- 基于 ZooKeeper 自动分配(临时顺序节点)
- 基于 IP+端口计算(小规模系统)
- 配置中心管理(大型系统)
-
号段模式:如何避免多实例号段重叠?
- 数据库行锁保证原子性
- 通过业务类型隔离不同应用的 ID 区间
-
-
时钟同步与回拨问题
- 雪花算法:默认拒绝生成 ID,需要等待时钟追上
- UidGenerator:记录最大时间戳,拒绝小于该时间的请求
- Leaf:小回拨等待,大回拨切换备用 workerId
WorkerId 边界条件处理: 使用 ZooKeeper 分配 WorkerId 时,需要注意 10 位 WorkerId 的最大值限制(1023)。当系统实例数可能超过此限制时,应采取措施:
java
public class BoundedWorkerIdAssigner implements WorkerIdAssigner {
private static final long MAX_WORKER_ID = 1023; // 10位二进制最大值
@Override
public long assignWorkerId() {
try {
// 创建临时顺序节点获取ID
String path = zkClient.create()...
// 解析原始ID
long originalId = Long.parseLong(path.substring(...));
// 对最大值取模,确保不超过上限
return originalId % (MAX_WORKER_ID + 1);
} catch (Exception e) {
// 异常处理
}
}
}
系统级时钟同步解决方案: 除了算法层面处理时钟回拨,还应从系统层面解决:
- 部署高精度 NTP 服务:配置内网 NTP 服务器,定期同步时钟
- 时钟监控告警:监控服务器时钟同步状态,大幅偏差时及时告警
- 单向递增时钟:对于关键服务,可使用 TSC(Time Stamp Counter)等硬件特性,实现单调递增时钟
java
// 系统时钟监控示例
@Scheduled(fixedRate = 5000) // 每5秒检查一次
public void monitorClockDrift() {
long localTime = System.currentTimeMillis();
long ntpTime = getNtpTime(); // 获取NTP服务器时间
long drift = Math.abs(localTime - ntpTime);
if (drift > 1000) { // 偏差超过1秒
log.warn("Clock drift detected: {}ms, local: {}, ntp: {}",
drift, localTime, ntpTime);
// 触发告警
alertService.sendAlert("Clock drift detected: " + drift + "ms");
}
}
方案实战:构建高可用的分布式 ID 服务
在实际项目中,我们通常会将 ID 生成器封装为独立服务。下面是一个完整的实现方案:
- 使用 Spring Boot 构建 ID 服务:
java
@RestController
@RequestMapping("/api/id")
public class IdGeneratorController {
private final SnowflakeIdGenerator idGenerator;
public IdGeneratorController(SnowflakeIdGenerator idGenerator) {
this.idGenerator = idGenerator;
}
@GetMapping("/next")
public Result<Long> nextId() {
return Result.success(idGenerator.nextId());
}
@GetMapping("/batch")
public Result<List<Long>> batchIds(@RequestParam(defaultValue = "10") int size) {
if (size <= 0 || size > 1000) {
return Result.fail("批量大小必须在1-1000之间");
}
List<Long> ids = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
ids.add(idGenerator.nextId());
}
return Result.success(ids);
}
}
- 自动从 ZooKeeper 获取 workerId:
java
@Component
public class ZkWorkerIdAssigner implements WorkerIdAssigner, InitializingBean {
private static final String ZK_PATH = "/id-generator/worker-id";
private static final long MAX_WORKER_ID = 1023; // 10位二进制最大值
@Value("${spring.application.name}")
private String appName;
@Value("${server.port}")
private String port;
private final CuratorFramework zkClient;
public ZkWorkerIdAssigner(CuratorFramework zkClient) {
this.zkClient = zkClient;
}
@Override
public long assignWorkerId() {
try {
// 确保路径存在
if (zkClient.checkExists().forPath(ZK_PATH) == null) {
zkClient.create().creatingParentsIfNeeded().forPath(ZK_PATH);
}
// 创建临时顺序节点
String nodeName = appName + "-" + InetAddress.getLocalHost().getHostAddress() + ":" + port + "-";
String path = zkClient.create().withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
.forPath(ZK_PATH + "/" + nodeName);
// 获取序号作为workerId
String id = path.substring(path.lastIndexOf("/") + nodeName.length() + 1);
long workerId = Long.parseLong(id);
// 确保不超过最大值
return workerId % (MAX_WORKER_ID + 1);
} catch (Exception e) {
throw new RuntimeException("Failed to assign worker id", e);
}
}
@Override
public void afterPropertiesSet() throws Exception {
// 服务启动时自动分配workerId
}
}
- 增强版雪花算法(处理时钟回拨):
java
public class EnhancedSnowflakeIdGenerator {
private static final long BACK_TIME_THRESHOLD = 5L; // 5ms
private final SnowflakeIdGenerator primaryGenerator;
private final SnowflakeIdGenerator backupGenerator;
private boolean usingBackup = false;
public EnhancedSnowflakeIdGenerator(WorkerIdAssigner assigner) {
// 主ID生成器
long primaryWorkerId = assigner.assignWorkerId();
this.primaryGenerator = new SnowflakeIdGenerator(primaryWorkerId, 0);
// 备用ID生成器(workerId取反,保证与主workerId不同)
// 确保备用ID在相同数据中心但使用不同workerId段,避免ID信息混乱
long backupWorkerId = 512 + (primaryWorkerId % 512); // 使用高512段
this.backupGenerator = new SnowflakeIdGenerator(backupWorkerId, 0);
}
public long nextId() {
try {
return usingBackup ? backupGenerator.nextId() : primaryGenerator.nextId();
} catch (Exception e) {
if (e.getMessage() != null && e.getMessage().contains("Clock moved backwards")) {
// 解析回拨时间
Matcher matcher = Pattern.compile("\\d+").matcher(e.getMessage());
if (matcher.find()) {
long backTime = Long.parseLong(matcher.group());
if (backTime <= BACK_TIME_THRESHOLD) {
// 回拨时间小,等待时钟追上
try {
Thread.sleep(backTime + 1);
return usingBackup ? backupGenerator.nextId() : primaryGenerator.nextId();
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
} else {
// 回拨时间大,切换到备用生成器
usingBackup = true;
return backupGenerator.nextId();
}
}
}
throw e;
}
}
}
性能测试与对比
对于分布式 ID 方案,性能是重要考量因素。下面是不同方案的性能对比:
java
public class IdGeneratorBenchmark {
public static void main(String[] args) {
SnowflakeIdGenerator snowflake = new SnowflakeIdGenerator(1, 1);
UuidGenerator uuid = new UuidGenerator();
DbIdGenerator dbIdGenerator = new DbIdGenerator(dataSource);
SegmentIdGenerator segmentIdGenerator = new SegmentIdGenerator(dataSource);
// 预热
for (int i = 0; i < 10000; i++) {
snowflake.nextId();
uuid.generateId();
}
// 测试变量定义
final int ITERATIONS = 1_000_000; // 百万次调用
final int DB_ITERATIONS = 10_000; // 数据库测试减少次数
long startTime, endTime;
// 雪花算法性能测试
startTime = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
snowflake.nextId();
}
endTime = System.nanoTime();
double snowflakeTps = ITERATIONS * 1_000_000_000.0 / (endTime - startTime);
System.out.printf("雪花算法: %.2f TPS, 平均延迟: %.3f μs\n",
snowflakeTps, (endTime - startTime) / 1000.0 / ITERATIONS);
// UUID性能测试
startTime = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
uuid.generateId();
}
endTime = System.nanoTime();
double uuidTps = ITERATIONS * 1_000_000_000.0 / (endTime - startTime);
System.out.printf("UUID: %.2f TPS, 平均延迟: %.3f μs\n",
uuidTps, (endTime - startTime) / 1000.0 / ITERATIONS);
// 号段模式性能测试
startTime = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
segmentIdGenerator.generateId("ORDER");
}
endTime = System.nanoTime();
double segmentTps = ITERATIONS * 1_000_000_000.0 / (endTime - startTime);
System.out.printf("号段模式: %.2f TPS, 平均延迟: %.3f μs\n",
segmentTps, (endTime - startTime) / 1000.0 / ITERATIONS);
// 数据库性能测试
startTime = System.nanoTime();
for (int i = 0; i < DB_ITERATIONS; i++) {
dbIdGenerator.generateId("ORDER");
}
endTime = System.nanoTime();
double dbTps = DB_ITERATIONS * 1_000_000_000.0 / (endTime - startTime);
System.out.printf("数据库模式: %.2f TPS, 平均延迟: %.3f μs\n",
dbTps, (endTime - startTime) / 1000.0 / DB_ITERATIONS);
}
}
典型结果(仅供参考,实际性能取决于硬件配置):
- 雪花算法:约 500-800 万 TPS,延迟<1μs
- UUID:约 200-300 万 TPS,延迟 2-3μs
- 号段模式:约 100-300 万 TPS(缓存命中),延迟 3-5μs
- 数据库模式:约 500-2000 TPS,延迟 500-2000μs
ID 安全与混淆
在某些场景下,直接暴露 ID 可能会泄露系统信息。如雪花算法生成的 ID 包含时间和机器信息,可通过混淆增强安全性:
java
public class IdObfuscator {
private final long PRIME = 7540113804746346429L; // 大质数
private final long MAX_LONG = Long.MAX_VALUE;
// 混淆ID(可逆)
public long obfuscate(long id) {
return (id ^ PRIME) & MAX_LONG;
}
// 还原ID
public long deobfuscate(long obfuscatedId) {
return (obfuscatedId ^ PRIME) & MAX_LONG;
}
}
对于更高安全级别,可考虑不可逆加密或哈希混淆,但需权衡 ID 长度和性能。
避坑指南
在实际应用中,有几个常见问题需要注意:
-
时钟回拨问题:服务器时间可能因为 NTP 同步等原因回调,导致 ID 重复。解决方案包括:
- 使用备用 workerId
- 等待时钟追上
- 记录最后时间戳,拒绝生成 ID 直到时钟追上
- 部署高精度 NTP 服务,监控时钟偏差,及时告警
-
workerId 分配问题:如何合理分配 workerId?
- 使用 ZooKeeper 等配置中心自动分配
- 通过机器 IP 等信息计算
- 手动配置(小规模场景)
- 确保分配 ID 不超过最大值限制(对大型系统尤为重要)
-
ID 信息安全:ID 中包含的信息可能泄露系统信息,如:
- 生成时间
- 机器编号
- 并发量
对安全性要求高的系统,可考虑对 ID 进行加密或混淆。
-
ID 存储长度:
- 数据库中 ID 字段的长度要合理设置,雪花算法生成的 ID 一般是 19 位左右的长整型
- 考虑到 Java 的 long 类型最大值为 9223372036854775807(19 位),需要留有余量
总结
方案 | ID 长度 | 性能(TPS) | 分布式扩展性 | 时间复杂度 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|---|---|---|
UUID | 32 字符 | ~300 万 | 极佳 | O(1) | 简单,无依赖 | 长度过长,无序 | 简单系统 |
数据库自增 | 8-20 位数字 | ~1000 | 差 | O(1) | 递增,实现简单 | 依赖数据库,性能低 | 数据量小系统 |
号段模式 | 8-20 位数字 | ~200 万 | 中等 | O(1) | 减少数据库压力 | 应用重启浪费 ID | 中高并发系统 |
Redis | 8-20 位数字 | ~10 万 | 中等 | O(1) | 性能好,递增 | 依赖 Redis | 中高并发系统 |
雪花算法 | 19 位数字 | ~600 万 | 良好 | O(1) | 高性能,无依赖 | 依赖时钟 | 高并发系统 |
UidGenerator | 18-20 位数字 | ~800 万 | 良好 | O(1) | 性能极高 | 实现复杂 | 超高并发系统 |
Leaf | 取决于配置 | ~500 万 | 优秀 | O(1) | 双模式支持 | 依赖组件多 | 企业级系统 |
混合方案 | 可配置 | 500-700 万 | 极佳 | O(1) | 灵活,高可用 | 实现复杂 | 超大规模系统 |
选择一个合适的分布式 ID 生成方案,应该根据系统的实际需求、性能要求和可用资源来决定。大多数中小型系统,使用雪花算法已经足够;而对于大型分布式系统,可以考虑使用 UidGenerator、Leaf 或混合方案。