分布式ID生成详解
一、知识概述
在分布式系统中,唯一ID的生成是一个基础且关键的问题。无论是订单号、用户ID、消息ID,还是数据库分片键,都需要一个全局唯一的标识符。一个好的分布式ID方案需要满足全局唯一、趋势递增、高性能、高可用等要求。
本文将深入讲解主流的分布式ID生成方案,包括UUID、数据库自增、号段模式、雪花算法、Leaf方案等,分析各种方案的优缺点和适用场景,帮助你在实际项目中做出正确的技术选型。
二、ID生成需求分析
2.1 核心需求
/**
* 分布式ID核心需求
*/
public class IDRequirements {
/**
* 1. 全局唯一性
* - 这是最基本的要求
* - 在整个分布式系统中唯一
* - 不能出现重复ID
*/
/**
* 2. 趋势递增
* - 有利于数据库索引
* - 减少页分裂
* - 提高查询性能
*/
/**
* 3. 高性能
* - 生成速度快
* - 低延迟
* - 高吞吐量
*/
/**
* 4. 高可用
* - 单点故障不影响ID生成
* - 容错能力强
* - 99.99%以上可用性
*/
/**
* 5. 信息安全
* - ID不暴露业务信息
* - 不暴露系统规模
* - 难以猜测
*/
/**
* 6. 可读性(可选)
* - 某些场景需要人工识别
* - 如订单号、发票号
*/
}
2.2 方案对比
/**
* 分布式ID方案对比
*/
public class IDSchemesComparison {
/*
┌─────────────────────────────────────────────────────────────────────────────┐
│ 分布式ID方案对比 │
├──────────────┬──────────┬──────────┬──────────┬──────────┬─────────────────┤
│ 方案 │ 全局唯一 │ 趋势递增 │ 性能 │ 可用性 │ 适用场景 │
├──────────────┼──────────┼──────────┼──────────┼──────────┼─────────────────┤
│ UUID │ ✓ │ ✗ │ 高 │ 高 │ 非主键、无序场景│
│ 数据库自增 │ ✓ │ ✓ │ 低 │ 低 │ 单库、小规模 │
│ 号段模式 │ ✓ │ ✓ │ 高 │ 中 │ 中等规模 │
│ 雪花算法 │ ✓ │ ✓ │ 高 │ 高 │ 大规模分布式 │
│ Leaf │ ✓ │ ✓ │ 高 │ 高 │ 大规模、高可用 │
│ TinyID │ ✓ │ ✓ │ 极高 │ 高 │ 超高并发 │
└──────────────┴──────────┴──────────┴──────────┴──────────┴─────────────────┘
*/
}
三、UUID方案
3.1 UUID 简介
/**
* UUID(Universally Unique Identifier)
*
* 特点:
* - 128位(16字节)
* - 32个十六进制字符 + 4个连字符 = 36个字符
* - 无需中心化协调
* - 本地生成,高性能
*/
public class UUIDSolution {
/**
* UUID 版本
*/
public void uuidVersions() {
// UUID v1: 基于时间戳和MAC地址
// 格式:时间戳 + 时钟序列 + 节点ID(MAC地址)
// 优点:有序
// 缺点:暴露MAC地址
// UUID v3: 基于命名空间和MD5哈希
// 格式:MD5(命名空间 + 名称)
// UUID v4: 随机生成(最常用)
// 格式:随机数
// 优点:简单、安全
// 缺点:无序
// UUID v5: 基于命名空间和SHA-1哈希
// 格式:SHA-1(命名空间 + 名称)
}
/**
* Java 生成 UUID
*/
public void generateUUID() {
// Java 默认生成 v4 UUID
UUID uuid = UUID.randomUUID();
String uuidStr = uuid.toString();
// 输出:550e8400-e29b-41d4-a716-446655440000
// 去掉连字符
String uuidNoDash = uuidStr.replace("-", "");
// 输出:550e8400e29b41d4a716446655440000
// 获取字节数组
byte[] bytes = uuidToBytes(uuid);
}
/**
* UUID 转字节数组
*/
private byte[] uuidToBytes(UUID uuid) {
ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
bb.putLong(uuid.getMostSignificantBits());
bb.putLong(uuid.getLeastSignificantBits());
return bb.array();
}
/**
* UUID 优缺点分析
*/
public void prosAndCons() {
// 优点:
// 1. 本地生成,无网络开销
// 2. 全局唯一
// 3. 无需中心化协调
// 4. 性能极高
// 缺点:
// 1. 无序,不利于数据库索引
// 2. 字符串过长(36字符)
// 3. 作为主键时索引性能差
// 4. 无法体现业务含义
// 5. v1版本可能暴露MAC地址
}
/**
* UUID 作为主键的问题
*/
public void uuidPrimaryKeyProblem() {
// MySQL InnoDB 主键是聚簇索引
// 数据按主键顺序存储
// UUID 无序导致:
// 1. 插入时需要大量随机IO
// 2. 频繁的页分裂
// 3. 索引碎片化
// 4. 空间利用率低
// 解决方案:
// 1. 使用有序UUID(UUID v1或改写v4)
// 2. 使用自增主键,UUID作为业务键
// 3. 使用COMB(UUID + 时间戳)
}
}
3.2 有序UUID实现
/**
* 有序UUID实现
*/
public class OrderedUUID {
/**
* COMB(UUID + 时间戳)
* 将时间戳作为UUID的一部分,使其有序
*/
public static UUID createCOMB() {
// 生成随机UUID
UUID uuid = UUID.randomUUID();
// 获取当前时间戳(精确到毫秒)
long timestamp = System.currentTimeMillis();
// 将时间戳嵌入UUID的后64位
long mostSigBits = uuid.getMostSignificantBits();
long leastSigBits = timestamp << 16 |
(uuid.getLeastSignificantBits() & 0xFFFF);
return new UUID(mostSigBits, leastSigBits);
}
/**
* 时间有序UUID(模拟v1)
*/
public static UUID createTimeOrderedUUID() {
// 时间戳(100纳秒精度,从1582-10-15开始)
long timestamp = System.currentTimeMillis();
long uuidTime = (timestamp - UUID_EPOCH_START) * 10000;
// 时钟序列
int clockSeq = ThreadLocalRandom.current().nextInt(0, 16384);
// 节点ID(可以用机器ID替代MAC地址)
long node = getNodeId();
// 组装UUID
long mostSigBits = (uuidTime << 32) |
(0x1000L << 48) | // 版本1
(clockSeq << 48 >>> 16);
long leastSigBits = (0x8000000000000000L) | // 变体
node;
return new UUID(mostSigBits, leastSigBits);
}
private static final long UUID_EPOCH_START =
GregorianCalendar.getInstance().getTimeInMillis();
private static long getNodeId() {
// 可以使用机器ID或随机数
return ThreadLocalRandom.current().nextLong(0, 281474976710656L);
}
/**
* 性能测试
*/
public static void main(String[] args) {
// 生成100万个UUID
long start = System.nanoTime();
for (int i = 0; i < 1_000_000; i++) {
UUID.randomUUID();
}
long end = System.nanoTime();
System.out.println("UUID生成100万耗时: " +
(end - start) / 1_000_000 + "ms");
}
}
四、数据库自增方案
4.1 单机自增
/**
* 数据库自增ID
*/
public class DatabaseAutoIncrement {
/**
* MySQL 自增主键
*/
/*
CREATE TABLE `id_generator` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`biz_type` varchar(64) NOT NULL COMMENT '业务类型',
`max_id` bigint(20) NOT NULL COMMENT '当前最大ID',
`step` int(11) NOT NULL COMMENT '步长',
`version` int(11) NOT NULL COMMENT '版本号',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_biz_type` (`biz_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
*/
/**
* 单机自增的问题
*/
public void singleMachineProblems() {
// 1. 单点故障
// - 数据库宕机后无法生成ID
// 2. 性能瓶颈
// - 单机QPS有限
// - 写入压力集中
// 3. 扩展困难
// - 无法水平扩展
}
/**
* 主从模式
*/
// 主库:auto_increment_increment=2, auto_increment_offset=1
// 从库:auto_increment_increment=2, auto_increment_offset=2
// 结果:主库生成1,3,5,7... 从库生成2,4,6,8...
}
4.2 号段模式
/**
* 号段模式(Flickr方案)
*
* 原理:
* - 每次从数据库获取一个号段
* - 在内存中分配ID
* - 号段用完再获取新号段
*/
public class SegmentMode {
/**
* 号段表设计
*/
/*
CREATE TABLE `id_segment` (
`biz_type` varchar(64) NOT NULL COMMENT '业务类型',
`max_id` bigint(20) NOT NULL COMMENT '当前最大ID',
`step` int(11) NOT NULL COMMENT '步长',
`version` int(11) NOT NULL COMMENT '版本号(乐观锁)',
PRIMARY KEY (`biz_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
*/
/**
* 号段服务
*/
@Service
public class SegmentIDService {
@Autowired
private JdbcTemplate jdbcTemplate;
// 内存中的号段
private final Map<String, Segment> segments = new ConcurrentHashMap<>();
/**
* 获取ID
*/
public synchronized long nextId(String bizType) {
Segment segment = segments.get(bizType);
// 如果号段为空或已用完,获取新号段
if (segment == null || !segment.hasNext()) {
segment = loadSegment(bizType);
segments.put(bizType, segment);
}
return segment.nextId();
}
/**
* 从数据库加载号段
*/
private Segment loadSegment(String bizType) {
// 使用乐观锁更新
String sql = "UPDATE id_segment SET max_id = max_id + step, " +
"version = version + 1 " +
"WHERE biz_type = ? AND version = ?";
while (true) {
// 先查询当前状态
Map<String, Object> current = jdbcTemplate.queryForMap(
"SELECT * FROM id_segment WHERE biz_type = ?", bizType);
long maxId = (Long) current.get("max_id");
int step = (Integer) current.get("step");
int version = (Integer) current.get("version");
// 更新
int updated = jdbcTemplate.update(sql, bizType, version);
if (updated > 0) {
// 更新成功,返回新号段
return new Segment(maxId, maxId + step);
}
// 乐观锁冲突,重试
}
}
/**
* 号段对象
*/
@Data
@AllArgsConstructor
private static class Segment {
private long startId;
private long endId;
private AtomicLong currentId;
public Segment(long startId, long endId) {
this.startId = startId;
this.endId = endId;
this.currentId = new AtomicLong(startId);
}
public boolean hasNext() {
return currentId.get() < endId;
}
public long nextId() {
return currentId.getAndIncrement();
}
}
}
/**
* 双Buffer优化
*/
@Service
public class DoubleBufferSegmentService {
private final Map<String, SegmentBuffer> buffers =
new ConcurrentHashMap<>();
/**
* 获取ID
*/
public long nextId(String bizType) {
SegmentBuffer buffer = buffers.computeIfAbsent(
bizType, k -> new SegmentBuffer());
return buffer.nextId();
}
/**
* 双Buffer
*/
private class SegmentBuffer {
private Segment[] segments = new Segment[2];
private volatile int currentPos = 0; // 当前使用的buffer
private volatile boolean nextReady = false; // 下一个buffer是否准备好
private volatile boolean isLoading = false; // 是否正在加载
public synchronized long nextId() {
Segment current = segments[currentPos];
// 当前buffer使用超过10%,异步加载下一个
if (current != null &&
current.getUsedPercent() > 0.1 &&
!nextReady && !isLoading) {
isLoading = true;
asyncLoadNext();
}
// 当前buffer用完,切换到下一个
if (current == null || !current.hasNext()) {
if (!nextReady) {
// 同步加载
loadNext();
}
// 切换
currentPos = 1 - currentPos;
current = segments[currentPos];
nextReady = false;
}
return current.nextId();
}
private void asyncLoadNext() {
CompletableFuture.runAsync(() -> {
loadNext();
nextReady = true;
isLoading = false;
});
}
private void loadNext() {
// 加载新号段
int nextPos = 1 - currentPos;
segments[nextPos] = loadSegmentFromDB();
}
}
}
}
五、雪花算法
5.1 雪花算法原理
/**
* 雪花算法(Snowflake)
*
* Twitter开源的分布式ID生成算法
* 64位Long类型ID
*/
public class SnowflakeAlgorithm {
/*
雪花算法ID结构(64位):
0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
│ │ │ │ │
│ │ │ │ └─ 12位序列号(毫秒内序列)
│ │ │ └───────── 5位机器ID(0-31)
│ │ └───────────────── 5位数据中心ID(0-31)
│ └───────────────────────────────────────────────────────────────────────── 41位时间戳(毫秒级)
└───────────────────────────────────────────────────────────────────────────────── 1位符号位(始终为0)
总计:1 + 41 + 5 + 5 + 12 = 64位
时间戳:41位,可用约69年
数据中心ID:5位,最大32个数据中心
机器ID:5位,每个数据中心最多32台机器
序列号:12位,每毫秒最多4096个ID
理论QPS:409.6万/秒
*/
/**
* 雪花算法实现
*/
public class SnowflakeIdGenerator {
// 起始时间戳(2024-01-01)
private final long twepoch = 1704038400000L;
// 位数分配
private final long workerIdBits = 5L;
private final long datacenterIdBits = 5L;
private final long sequenceBits = 12L;
// 最大值
private final long maxWorkerId = ~(-1L << workerIdBits); // 31
private final long maxDatacenterId = ~(-1L << datacenterIdBits); // 31
private final long sequenceMask = ~(-1L << sequenceBits); // 4095
// 位移
private final long workerIdShift = sequenceBits;
private final long datacenterIdShift = sequenceBits + workerIdBits;
private final long timestampLeftShift =
sequenceBits + workerIdBits + datacenterIdBits;
// 工作机器ID
private final long workerId;
private final long datacenterId;
// 序列号和上次时间戳
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(
"worker Id can't be greater than %d or less than 0");
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(
"datacenter Id can't be greater than %d or less than 0");
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
/**
* 生成ID(线程安全)
*/
public synchronized long nextId() {
long timestamp = timeGen();
// 时钟回拨检测
if (timestamp < lastTimestamp) {
throw new RuntimeException(
"Clock moved backwards, refusing to generate id");
}
// 同一毫秒内
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & sequenceMask;
// 序列号溢出,等待下一毫秒
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
// 新毫秒,序列号重置
sequence = 0L;
}
lastTimestamp = timestamp;
// 组装ID
return ((timestamp - twepoch) << timestampLeftShift) |
(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();
}
/**
* 解析ID
*/
public IdInfo parseId(long id) {
long timestamp = (id >>> timestampLeftShift) + twepoch;
long datacenter = (id >>> datacenterIdShift) & maxDatacenterId;
long worker = (id >>> workerIdShift) & maxWorkerId;
long seq = id & sequenceMask;
return new IdInfo(timestamp, datacenter, worker, seq);
}
@Data
@AllArgsConstructor
public static class IdInfo {
private long timestamp;
private long datacenterId;
private long workerId;
private long sequence;
}
}
/**
* 使用示例
*/
public void usage() {
// 创建生成器
SnowflakeIdGenerator generator =
new SnowflakeIdGenerator(1, 1);
// 生成ID
long id = generator.nextId();
System.out.println("ID: " + id);
// 解析ID
IdInfo info = generator.parseId(id);
System.out.println("解析结果: " + info);
}
}
5.2 时钟回拨问题
/**
* 时钟回拨问题及解决方案
*/
public class ClockBackwardSolution {
/**
* 时钟回拨原因
*/
// 1. NTP时间同步
// 2. 人工调整时间
// 3. 虚拟机时间漂移
// 4. 硬件时钟问题
/**
* 方案1:等待时间追上
*/
public class WaitSolution extends SnowflakeIdGenerator {
private static final long MAX_BACKWARD_MS = 5; // 最大容忍回拨时间
@Override
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
long offset = lastTimestamp - timestamp;
if (offset <= MAX_BACKWARD_MS) {
// 小幅回拨,等待
try {
Thread.sleep(offset);
timestamp = timeGen();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
// 大幅回拨,拒绝
throw new RuntimeException("Clock moved backwards");
}
}
// 正常生成逻辑...
return generateId(timestamp);
}
}
/**
* 方案2:使用预留序号
*/
public class ReservedSequenceSolution extends SnowflakeIdGenerator {
// 上一次时间戳对应的最大序列号
private long lastMaxSequence = 0;
@Override
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
// 时钟回拨,使用之前预留的序列号
if (lastMaxSequence < sequenceMask) {
sequence = lastMaxSequence + 1;
return generateId(timestamp);
}
// 序列号用完,抛异常
throw new RuntimeException("Clock moved backwards and sequence exhausted");
}
long id = generateId(timestamp);
// 正常情况下,预留当前时间戳的序列号范围
if (timestamp > lastTimestamp) {
lastMaxSequence = 0;
}
return id;
}
}
/**
* 方案3:动态调整机器ID
*/
public class DynamicWorkerIdSolution {
private final WorkerIdAllocator workerIdAllocator;
private volatile long workerId;
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
// 时钟回拨,重新分配workerId
workerId = workerIdAllocator.allocate();
// 重置lastTimestamp
lastTimestamp = timestamp;
}
// 正常生成...
return generateId(timestamp);
}
}
/**
* 方案4:Baidu UidGenerator方案
* 使用时间戳秒级而非毫秒,增加序列号位数
*/
public class UidGeneratorSolution {
// 时间戳改为秒级,节省位数
// 序列号从12位增加到22位
// 可承受1秒的时钟回拨
}
}
5.3 机器ID分配
/**
* 机器ID分配方案
*/
public class WorkerIdAllocation {
/**
* 方案1:配置文件
*/
// 每台机器配置不同的workerId
// 缺点:运维成本高,容易冲突
/**
* 方案2:数据库分配
*/
@Service
public class DatabaseWorkerIdAllocator implements WorkerIdAllocator {
@Autowired
private JdbcTemplate jdbcTemplate;
/*
CREATE TABLE `worker_id_allocator` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`ip` varchar(64) NOT NULL COMMENT '机器IP',
`worker_id` int(11) NOT NULL COMMENT '分配的workerId',
`create_time` datetime NOT NULL,
`expire_time` datetime NOT NULL COMMENT '过期时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_worker_id` (`worker_id`),
UNIQUE KEY `uk_ip` (`ip`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
*/
@Override
public long allocate() {
String ip = getLocalIp();
// 尝试获取已分配的
Long workerId = getExistingWorkerId(ip);
if (workerId != null) {
renewWorkerId(ip);
return workerId;
}
// 分配新的
return allocateNewWorkerId(ip);
}
private Long getExistingWorkerId(String ip) {
try {
return jdbcTemplate.queryForObject(
"SELECT worker_id FROM worker_id_allocator " +
"WHERE ip = ? AND expire_time > NOW()",
Long.class, ip);
} catch (EmptyResultDataAccessException e) {
return null;
}
}
private long allocateNewWorkerId(String ip) {
// 寻找未使用的workerId
for (int i = 0; i < 32; i++) {
try {
jdbcTemplate.update(
"INSERT INTO worker_id_allocator " +
"(ip, worker_id, create_time, expire_time) " +
"VALUES (?, ?, NOW(), DATE_ADD(NOW(), INTERVAL 1 DAY))",
ip, i);
return i;
} catch (DuplicateKeyException e) {
// workerId已被占用,继续尝试
continue;
}
}
throw new RuntimeException("No available workerId");
}
private void renewWorkerId(String ip) {
jdbcTemplate.update(
"UPDATE worker_id_allocator SET expire_time = " +
"DATE_ADD(NOW(), INTERVAL 1 DAY) WHERE ip = ?", ip);
}
}
/**
* 方案3:Redis分配
*/
@Service
public class RedisWorkerIdAllocator implements WorkerIdAllocator {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String WORKER_ID_KEY = "snowflake:worker_id";
private static final int MAX_WORKER_ID = 31;
@Override
public long allocate() {
String ip = getLocalIp();
String key = WORKER_ID_KEY + ":" + ip;
// 尝试获取已有的
String existing = redisTemplate.opsForValue().get(key);
if (existing != null) {
// 续期
redisTemplate.expire(key, 1, TimeUnit.DAYS);
return Long.parseLong(existing);
}
// 分配新的
for (int i = 0; i <= MAX_WORKER_ID; i++) {
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(WORKER_ID_KEY + ":slot:" + i, ip,
1, TimeUnit.DAYS);
if (Boolean.TRUE.equals(success)) {
// 绑定IP
redisTemplate.opsForValue()
.set(key, String.valueOf(i), 1, TimeUnit.DAYS);
return i;
}
}
throw new RuntimeException("No available workerId");
}
}
/**
* 方案4:Zookeeper分配
*/
@Service
public class ZkWorkerIdAllocator implements WorkerIdAllocator {
@Autowired
private CuratorFramework zkClient;
private static final String PATH = "/snowflake/worker_id";
@Override
public long allocate() {
try {
// 创建临时顺序节点
String node = zkClient.create()
.creatingParentsIfNeeded()
.withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
.forPath(PATH + "/worker-");
// 获取序号
String workerIdStr = node.substring(node.lastIndexOf('-') + 1);
long workerId = Long.parseLong(workerIdStr) % 32;
return workerId;
} catch (Exception e) {
throw new RuntimeException("Failed to allocate workerId", e);
}
}
}
}
六、Leaf方案
6.1 Leaf 简介
/**
* Leaf - 美团分布式ID生成系统
*
* 提供两种模式:
* 1. Leaf-segment:号段模式,适合对ID连续性有要求的场景
* 2. Leaf-snowflake:雪花算法模式,适合对性能要求高的场景
*/
6.2 Leaf-segment 实现
/**
* Leaf-segment 号段模式实现
*/
@Service
public class LeafSegmentService {
/**
* 号段表设计
*/
/*
CREATE TABLE `leaf_segment` (
`biz_tag` varchar(128) NOT NULL COMMENT '业务标识',
`max_id` bigint(20) NOT NULL COMMENT '当前最大ID',
`step` int(11) NOT NULL COMMENT '步长',
`description` varchar(256) DEFAULT NULL,
`update_time` datetime NOT NULL,
PRIMARY KEY (`biz_tag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
*/
/**
* 双Buffer号段
*/
@Data
public class SegmentBuffer {
private String key; // 业务标识
private Segment[] segments = new Segment[2]; // 双buffer
private volatile int currentPos = 0; // 当前使用的buffer索引
private volatile boolean nextReady = false; // 下一个buffer是否准备好
private volatile boolean loading = false; // 是否正在加载
private final ReadWriteLock lock = new ReentrantReadWriteLock();
public Segment getCurrent() {
return segments[currentPos];
}
public Segment getNext() {
return segments[1 - currentPos];
}
public void switchBuffer() {
currentPos = 1 - currentPos;
}
}
@Data
public class Segment {
private AtomicLong value; // 当前值
private long max; // 最大值
private long step; // 步长
private volatile boolean initialized = false;
public Segment(long start, long step) {
this.value = new AtomicLong(start);
this.max = start + step;
this.step = step;
this.initialized = true;
}
public boolean hasNext() {
return value.get() < max;
}
public long nextId() {
return value.getAndIncrement();
}
public double getUsedPercent() {
return (value.get() - (max - step)) * 1.0 / step;
}
}
private final Map<String, SegmentBuffer> buffers =
new ConcurrentHashMap<>();
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 获取ID
*/
public long nextId(String key) {
SegmentBuffer buffer = buffers.computeIfAbsent(
key, k -> initBuffer(k));
return getIdFromBuffer(buffer);
}
private long getIdFromBuffer(SegmentBuffer buffer) {
while (true) {
buffer.lock.readLock().lock();
try {
Segment current = buffer.getCurrent();
if (!current.initialized) {
// 当前buffer未初始化,等待
continue;
}
// 当前buffer有剩余
if (current.hasNext()) {
long id = current.nextId();
// 使用超过10%,异步加载下一个buffer
if (!buffer.nextReady &&
!buffer.loading &&
current.getUsedPercent() > 0.1) {
buffer.loading = true;
asyncLoadNextBuffer(buffer);
}
return id;
}
// 当前buffer用完,检查下一个buffer是否准备好
if (buffer.nextReady) {
buffer.lock.readLock().unlock();
buffer.lock.writeLock().lock();
try {
// 切换buffer
buffer.switchBuffer();
buffer.nextReady = false;
buffer.loading = false;
} finally {
buffer.lock.readLock().lock();
buffer.lock.writeLock().unlock();
}
continue;
}
// 下一个buffer也未准备好,同步加载
return loadAndGetId(buffer);
} finally {
buffer.lock.readLock().unlock();
}
}
}
/**
* 异步加载下一个buffer
*/
private void asyncLoadNextBuffer(SegmentBuffer buffer) {
CompletableFuture.runAsync(() -> {
try {
Segment next = loadSegment(buffer.getKey());
buffer.lock.writeLock().lock();
try {
buffer.segments[1 - buffer.currentPos] = next;
buffer.nextReady = true;
} finally {
buffer.lock.writeLock().unlock();
}
} catch (Exception e) {
// 加载失败,下次重试
buffer.loading = false;
}
});
}
/**
* 同步加载并获取ID
*/
private long loadAndGetId(SegmentBuffer buffer) {
buffer.lock.readLock().unlock();
buffer.lock.writeLock().lock();
try {
// 再次检查
Segment current = buffer.getCurrent();
if (current.hasNext()) {
return current.nextId();
}
// 加载新号段
Segment segment = loadSegment(buffer.getKey());
buffer.segments[buffer.currentPos] = segment;
return segment.nextId();
} finally {
buffer.lock.readLock().lock();
buffer.lock.writeLock().unlock();
}
}
/**
* 从数据库加载号段
*/
private Segment loadSegment(String key) {
// 更新数据库
jdbcTemplate.update(
"UPDATE leaf_segment SET max_id = max_id + step, " +
"update_time = NOW() WHERE biz_tag = ?", key);
// 读取新号段
Map<String, Object> result = jdbcTemplate.queryForMap(
"SELECT max_id, step FROM leaf_segment WHERE biz_tag = ?", key);
long maxId = (Long) result.get("max_id");
int step = (Integer) result.get("step");
return new Segment(maxId - step, step);
}
private SegmentBuffer initBuffer(String key) {
SegmentBuffer buffer = new SegmentBuffer();
buffer.setKey(key);
buffer.segments[0] = loadSegment(key);
return buffer;
}
}
6.3 Leaf-snowflake 实现
/**
* Leaf-snowflake 雪花算法实现
*/
@Service
public class LeafSnowflakeService {
// 使用Zookeeper管理workerId
@Autowired
private CuratorFramework zkClient;
private static final String ZK_PATH = "/leaf/snowflake";
private SnowflakeIdGenerator generator;
@PostConstruct
public void init() throws Exception {
// 获取workerId
long workerId = getWorkerId();
// 初始化生成器
generator = new SnowflakeIdGenerator(workerId, 0);
}
/**
* 从Zookeeper获取workerId
*/
private long getWorkerId() throws Exception {
// 获取本机IP
String ip = getLocalIp();
// 创建临时节点
String node = zkClient.create()
.creatingParentsIfNeeded()
.withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
.forPath(ZK_PATH + "/" + ip + "-");
// 解析workerId
String workerIdStr = node.substring(node.lastIndexOf('-') + 1);
return Long.parseLong(workerIdStr) % 32;
}
/**
* 生成ID
*/
public long nextId() {
return generator.nextId();
}
}
七、最佳实践与选型
7.1 选型建议
/**
* 分布式ID选型建议
*/
public class IDSelectionGuide {
/**
* 选型决策树
*/
public String selectIdScheme(Requirement req) {
// 1. 是否需要作为数据库主键?
if (req.isPrimaryKey()) {
// 需要趋势递增
if (req.getQps() < 1000) {
// QPS较低,使用号段模式
return "Segment";
} else {
// QPS较高,使用雪花算法
if (req.canTolerateClockBackward()) {
return "Snowflake";
} else {
// 对时钟敏感,使用Leaf
return "Leaf-Segment";
}
}
} else {
// 不作为主键
if (req.needSort()) {
// 需要排序
return "Snowflake";
} else {
// 不需要排序,UUID即可
return "UUID";
}
}
}
/**
* 各方案适用场景
*/
/*
┌─────────────────────────────────────────────────────────────────────────┐
│ 分布式ID方案适用场景 │
├──────────────┬──────────────────────────────────────────────────────────┤
│ 方案 │ 适用场景 │
├──────────────┼──────────────────────────────────────────────────────────┤
│ UUID │ 非主键、无需排序、简单场景 │
│ 数据库自增 │ 单机、小规模、已有数据库依赖 │
│ 号段模式 │ 中等规模、需要趋势递增、对可用性有一定要求 │
│ 雪花算法 │ 大规模分布式、高并发、能容忍一定程度的时钟回拨 │
│ Leaf │ 大规模、高可用、需要完善的监控和管理 │
└──────────────┴──────────────────────────────────────────────────────────┘
*/
}
7.2 注意事项
/**
* 分布式ID使用注意事项
*/
public class IDBestPractices {
/**
* 1. 时钟同步
*/
// 使用雪花算法时,必须保证时钟同步
// 配置NTP服务
// 监控时钟偏移
/**
* 2. 机器ID管理
*/
// 确保机器ID不重复
// 支持动态分配和回收
// 容器环境需要特别注意
/**
* 3. 监控告警
*/
// ID生成QPS监控
// 号段使用率监控
// 时钟回拨告警
/**
* 4. 容灾设计
*/
// 多机房部署
// 故障自动切换
// 数据持久化
/**
* 5. 性能优化
*/
// 本地缓存
// 批量获取
// 异步加载
}
六、思考与练习
思考题
-
基础题:UUID作为主键存在哪些性能问题?为什么InnoDB引擎对UUID主键不友好?
-
进阶题:雪花算法的时间戳只有41位,可用约69年。如果系统需要运行更长时间,有哪些解决方案?
-
实战题:在Kubernetes容器环境中部署使用雪花算法的服务,workerId如何动态分配和管理?请设计一个完整方案。
编程练习
练习:实现一个完整的雪花算法ID生成器,包含:(1) 时钟回拨检测与处理;(2) workerId自动分配(基于Redis或Zookeeper);(3) ID解析工具,能从ID中提取时间戳、机器ID等信息。
章节关联
- 前置章节:CAP与BASE理论详解
- 后续章节:分布式锁实现详解
- 扩展阅读:Twitter Snowflake源码、美团Leaf技术博客
📝 下一章预告
当多个进程同时访问共享资源时,如何保证数据一致性?下一章将深入讲解分布式锁的实现原理,包括数据库锁、Redis锁、Zookeeper锁等主流方案。
本章完
参考资料:
- Twitter Snowflake: github.com/twitter-arc…
- 美团 Leaf: github.com/Meituan-Dia…
- 百度 UidGenerator: github.com/baidu/uid-g…
- 滴滴 TinyID: github.com/didi/tinyid