一、为什么需要分布式ID
在分布式系统中,数据库自增ID已经无法满足需求:
自增ID的问题:
- 分库分表后ID冲突:不同库的自增ID会重复
- 暴露业务信息:连续ID暴露订单量等敏感信息
- 单点瓶颈:依赖数据库生成ID,性能受限
- 无法排序:无法通过ID判断创建时间
分布式ID的要求:
- 全局唯一:不同机器生成的ID不重复
- 趋势递增:便于数据库索引优化
- 高性能:每秒生成百万级ID
- 高可用:不依赖单点
- 信息安全:不暴露业务信息
二、雪花算法(Snowflake)
1. 算法原理
64位ID结构:
┌─────────────────────────────────────────────────────────────────┐
│ 0 │ 41位时间戳 │ 10位机器ID │ 12位序列号 │
└─────────────────────────────────────────────────────────────────┘
1位 41位 10位 12位
符号位 毫秒时间戳 机器标识 序列号
各部分说明:
- 1位符号位:固定为0,保证ID为正数
- 41位时间戳:毫秒级,可用约69年
- 10位机器ID:支持1024台机器(5位数据中心+5位机器)
- 12位序列号:同一毫秒内最多生成4096个ID
理论最大QPS: 4096 × 1000 = 4,096,000/秒
2. Java实现
java
public class SnowflakeIdGenerator {
// 开始时间戳(2020-01-01 00:00:00)
private static final long START_TIMESTAMP = 1577836800000L;
// 各部分占用位数
private static final long SEQUENCE_BITS = 12L;
private static final long MACHINE_BITS = 5L;
private static final long DATACENTER_BITS = 5L;
// 最大值
private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS); // 4095
private static final long MAX_MACHINE_ID = ~(-1L << MACHINE_BITS); // 31
private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTER_BITS); // 31
// 位移量
private static final long MACHINE_SHIFT = SEQUENCE_BITS;
private static final long DATACENTER_SHIFT = SEQUENCE_BITS + MACHINE_BITS;
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + MACHINE_BITS + DATACENTER_BITS;
private final long datacenterId;
private final long machineId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(long datacenterId, long machineId) {
if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) {
throw new IllegalArgumentException("datacenterId超出范围");
}
if (machineId > MAX_MACHINE_ID || machineId < 0) {
throw new IllegalArgumentException("machineId超出范围");
}
this.datacenterId = datacenterId;
this.machineId = machineId;
}
public synchronized long nextId() {
long currentTimestamp = System.currentTimeMillis();
// 时钟回拨检测
if (currentTimestamp < lastTimestamp) {
throw new RuntimeException(
String.format("时钟回拨,拒绝生成ID,回拨时间:%d毫秒",
lastTimestamp - currentTimestamp));
}
if (currentTimestamp == lastTimestamp) {
// 同一毫秒内,序列号递增
sequence = (sequence + 1) & MAX_SEQUENCE;
if (sequence == 0) {
// 序列号溢出,等待下一毫秒
currentTimestamp = waitNextMillis(lastTimestamp);
}
} else {
// 不同毫秒,序列号重置
sequence = 0L;
}
lastTimestamp = currentTimestamp;
// 组装ID
return ((currentTimestamp - START_TIMESTAMP) << TIMESTAMP_SHIFT)
| (datacenterId << DATACENTER_SHIFT)
| (machineId << MACHINE_SHIFT)
| sequence;
}
private long waitNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
// 解析ID
public static Map<String, Long> parseId(long id) {
Map<String, Long> result = new HashMap<>();
result.put("timestamp", (id >> TIMESTAMP_SHIFT) + START_TIMESTAMP);
result.put("datacenterId", (id >> DATACENTER_SHIFT) & MAX_DATACENTER_ID);
result.put("machineId", (id >> MACHINE_SHIFT) & MAX_MACHINE_ID);
result.put("sequence", id & MAX_SEQUENCE);
return result;
}
}
Spring Boot集成:
java
@Configuration
public class SnowflakeConfig {
@Value("${snowflake.datacenter-id:1}")
private long datacenterId;
@Value("${snowflake.machine-id:1}")
private long machineId;
@Bean
public SnowflakeIdGenerator snowflakeIdGenerator() {
return new SnowflakeIdGenerator(datacenterId, machineId);
}
}
@Service
public class OrderService {
@Autowired
private SnowflakeIdGenerator idGenerator;
public Order createOrder(OrderRequest request) {
Order order = new Order();
order.setId(idGenerator.nextId()); // 生成分布式ID
order.setUserId(request.getUserId());
// ...
return order;
}
}
3. 时钟回拨问题
问题: 服务器时钟回拨会导致ID重复
解决方案:
java
public synchronized long nextId() {
long currentTimestamp = System.currentTimeMillis();
if (currentTimestamp < lastTimestamp) {
long offset = lastTimestamp - currentTimestamp;
if (offset <= 5) {
// 回拨时间较短,等待时钟追上
try {
Thread.sleep(offset << 1);
currentTimestamp = System.currentTimeMillis();
if (currentTimestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨,无法生成ID");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("等待时钟恢复被中断");
}
} else {
// 回拨时间较长,直接报错
throw new RuntimeException(
String.format("时钟回拨超过5ms,回拨时间:%d ms", offset));
}
}
// ... 其余逻辑
}
三、美团Leaf
1. Leaf-Segment(号段模式)
原理:
数据库存储号段信息:
┌──────────────┬──────────┬──────────┬──────────┐
│ biz_tag │ max_id │ step │ version │
├──────────────┼──────────┼──────────┼──────────┤
│ order │ 1000000 │ 1000 │ 1 │
│ user │ 500000 │ 500 │ 1 │
└──────────────┴──────────┴──────────┴──────────┘
每次从数据库取一段号段(如1000个),在内存中分配
当号段用完时,再从数据库取下一段
数据库表:
sql
CREATE TABLE leaf_alloc (
biz_tag VARCHAR(128) NOT NULL DEFAULT '',
max_id BIGINT NOT NULL DEFAULT 1,
step INT NOT NULL,
description VARCHAR(256) DEFAULT NULL,
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (biz_tag)
) ENGINE=InnoDB;
-- 初始化数据
INSERT INTO leaf_alloc(biz_tag, max_id, step, description)
VALUES ('order', 1, 2000, '订单ID');
Java实现:
java
@Service
public class LeafSegmentService {
@Autowired
private LeafAllocMapper leafAllocMapper;
private Map<String, SegmentBuffer> cache = new ConcurrentHashMap<>();
public long nextId(String bizTag) {
SegmentBuffer buffer = cache.computeIfAbsent(bizTag,
k -> new SegmentBuffer(k));
return buffer.nextId();
}
private class SegmentBuffer {
private final String bizTag;
private volatile Segment current;
private volatile Segment next;
private volatile boolean isPreloading = false;
public SegmentBuffer(String bizTag) {
this.bizTag = bizTag;
this.current = loadSegment();
}
public synchronized long nextId() {
// 当前号段使用超过10%,预加载下一个号段
if (!isPreloading && current.getUsagePercent() > 0.1) {
isPreloading = true;
CompletableFuture.runAsync(() -> {
next = loadSegment();
isPreloading = false;
});
}
// 当前号段用完,切换到下一个
if (current.isExhausted()) {
if (next != null) {
current = next;
next = null;
} else {
current = loadSegment();
}
}
return current.nextId();
}
private Segment loadSegment() {
// 从数据库获取号段
LeafAlloc alloc = leafAllocMapper.updateMaxIdAndGetLeafAlloc(bizTag);
long min = alloc.getMaxId() - alloc.getStep() + 1;
long max = alloc.getMaxId();
return new Segment(min, max);
}
}
}
2. Leaf-Snowflake(雪花模式)
改进点: 使用Zookeeper解决机器ID分配问题
java
@Component
public class LeafSnowflakeService {
@Autowired
private ZookeeperClient zkClient;
private long workerId;
@PostConstruct
public void init() {
// 从Zookeeper获取workerId
String path = "/leaf/snowflake/" + getLocalIp();
if (zkClient.exists(path)) {
// 已注册,读取workerId
workerId = Long.parseLong(zkClient.getData(path));
} else {
// 新注册,分配workerId
workerId = zkClient.createSequential("/leaf/snowflake/worker-");
}
log.info("Leaf-Snowflake workerId: {}", workerId);
}
public long nextId() {
return snowflake.nextId(workerId);
}
}
四、百度UidGenerator
1. 原理
64位ID结构(可配置):
┌──────────────────────────────────────────────────────────────────┐
│ 1位 │ 28位时间戳 │ 22位机器ID │ 13位序列号 │
└──────────────────────────────────────────────────────────────────┘
特点:
- 时间戳精度可配置(秒级)
- 机器ID通过数据库分配
- 支持RingBuffer缓存,性能极高
2. 集成
xml
<dependency>
<groupId>com.baidu.fsg</groupId>
<artifactId>uid-generator</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
java
@Service
public class OrderService {
@Autowired
private UidGenerator uidGenerator;
public Order createOrder(OrderRequest request) {
Order order = new Order();
order.setId(uidGenerator.getUID());
// ...
return order;
}
}
五、各方案对比
| 方案 | 性能 | 依赖 | 时钟回拨 | 趋势递增 | 适用场景 |
|---|---|---|---|---|---|
| 雪花算法 | 极高 | 无 | 有风险 | 是 | 通用 |
| Leaf-Segment | 高 | MySQL | 无 | 是 | 号段分配 |
| Leaf-Snowflake | 极高 | ZK | 有处理 | 是 | 通用 |
| UidGenerator | 极高 | MySQL | 无 | 是 | 高性能 |
| Redis自增 | 高 | Redis | 无 | 是 | 简单场景 |
| UUID | 高 | 无 | 无 | 否 | 不推荐 |
六、Redis实现分布式ID
java
@Service
public class RedisIdGenerator {
@Autowired
private RedisTemplate<String, Long> redisTemplate;
public long nextId(String bizTag) {
String key = "id:" + bizTag + ":" + LocalDate.now().toString();
// 原子自增
Long id = redisTemplate.opsForValue().increment(key);
// 设置过期时间(防止key无限增长)
redisTemplate.expire(key, 1, TimeUnit.DAYS);
// 拼接时间戳,保证全局唯一
long timestamp = System.currentTimeMillis();
return timestamp * 1000000 + id;
}
}
七、总结
分布式ID是分布式系统的基础组件:
- 雪花算法:无依赖,高性能,注意时钟回拨
- Leaf-Segment:号段模式,依赖MySQL,稳定可靠
- Leaf-Snowflake:雪花改进版,依赖ZK,解决机器ID问题
- UidGenerator:百度开源,性能极高
选型建议:
- 简单场景:雪花算法
- 需要号段分配:Leaf-Segment
- 高性能场景:UidGenerator
思考题:你们系统用的什么分布式ID方案?有没有遇到过ID冲突的问题?
个人观点,仅供参考