目录
1、问题背景
2、主流分布式ID解决方案
2.2.1 UUID
2.2.2 数据库自增ID(多实例模式)
2.2.3 数据库号段模式(Segment / Leaf-Segment)
2.2.4 Snowflake(雪花算法)及其变种
2.2.5 基于Redis生成
2.2.6 开源中间件(如Tinyid, Leaf)
3、方案对比及技术选型
4、SpringBoot实现Snowflake算法
4.1 创建Snowflake算法实现类
4.2 创建配置类
4.3 配置文件
4.4 创建Service层
4.5 创建Controller(可选)
5、高级实现:解决Snowflake时钟回拨和WorkerID分配问题
5.1 解决时钟回拨:增强的Snowflake实现
5.2 动态WorkerID分配(基于数据库)
5.3 基于ZooKeeper的WorkerID分配
1、问题背景
在单机系统中,通常使用数据库的自增主键(AUTO_INCREMENT)来生成唯一ID。但在分布式系统(如微服务、分库分表)中,这种方法会遇到瓶颈:
性能瓶颈:所有ID生成请求都指向同一个数据库,数据库容易成为单点瓶颈。
可用性问题:如果数据库宕机,整个系统将无法生成新ID,业务停滞。
分库分表问题:如果多个数据库实例各自自增,会产生重复的ID,无法保证全局唯一。
因此,分布式ID需要满足以下核心要求:
全局唯一:这是最基本的要求,整个分布式系统中不能出现重复的ID。
高性能高可用:ID生成服务要能承受高并发,并且需要高可用,不能有单点故障。
趋势递增:多数场景下,有序的ID(如时间戳递增)能提升数据库写入性能(B+Tree索引)。
单调递增:保证下一个ID一定大于上一个ID,在某些严格时序场景下需要。
信息安全:如果ID是连续的,容易被恶意爬取数据,有时需要无规则性。
含时间信息:从ID本身能反推出生成时间,便于排查问题或数据归档。
2、主流分布式ID解决方案
2.2.1 UUID
UUID(Universally Unique Identifier)是一个128位的数字,标准形式包含32个十六进制数字,以连字号分为五段(例如:123e4567-e89b-12d3-a456-426614174000
)。
实现方式:本地生成,无需中心化服务。Java中可以直接使用
UUID.randomUUID()
。优点:
实现简单:无需任何依赖,代码简洁。
性能极高:本地生成,没有网络开销。
全局唯一:理论上是唯一的,重复概率极低可忽略。
缺点:
无序性:生成的ID是随机的,不具备递增趋势。作为数据库主键时,会严重影响写入性能(导致B+Tree频繁页分裂)。
存储空间大:128位(16字节),比长整型(8字节)大,占用更多存储和索引空间。
可读性差:是一串无意义的字符,无法从中获取任何有用信息。
适用场景:对性能要求极高、数据量不大、不需要有序、ID不作为数据库主键的场景,如生成临时令牌、会话ID等。
2.2.2 数据库自增ID(多实例模式)
通过设置不同的数据库实例的起始值和步长,来避免ID冲突。
实现方式:
数据库1:
auto_increment_offset = 1
,auto_increment_increment = 2
-> 生成 ID: 1, 3, 5, 7...数据库2:
auto_increment_offset = 2
,auto_increment_increment = 2
-> 生成 ID: 2, 4, 6, 8...
优点:
实现简单:基于现有数据库,成本低。
ID有序递增:有利于数据库性能。
缺点:
扩展性差:一旦确定步长,后续增加数据库实例会很麻烦,需要重新规划。
数据库压力:仍然是基于数据库,高并发下性能是瓶颈。
单点故障风险:每个数据库实例都是单点,虽然多个实例,但单个实例宕机影响其负责的ID段。
适用场景:数据库数量固定,并发量不高的简单分布式场景。不推荐作为主流方案。
2.2.3 数据库号段模式(Segment / Leaf-Segment)
这是对数据库方案的优化,不再每次获取ID都访问数据库,而是批量获取一个号段(ID范围),缓存在本地。
实现方式:
数据库中有一张表
id_generator
,包含biz_tag
(业务类型),max_id
(当前最大ID),step
(号段长度)。业务服务在需要ID时,不是获取一个,而是从数据库中获取一个号段(如
max_id
到max_id + step
)。本地服务将
max_id
到max_id + step
缓存在内存中,并更新数据库中的max_id
为max_id + step
。业务请求ID时,直接从本地内存中分配,直到号段用完,再去数据库获取新的号段。
优点:
大幅降低数据库压力:一次数据库交互可以支撑多个ID生成请求。
高性能:ID分配在本地内存完成,性能极高。
可以灵活调整步长:根据业务吞吐量动态调整
step
大小。
缺点:
服务重启可能导致ID空洞:如果服务重启,内存中未使用的号段ID会丢失。
ID不是严格递增,而是趋势递增(号段A用完后,才去取更大的号段B)。
仍然存在数据库单点问题(可通过主从切换解决)。
业界实践:美团的Leaf 就采用了双Buffer优化的号段模式,提前加载下一个号段,避免在号段切换时产生等待,实现了高可用和高性能。
2.2.4 Snowflake(雪花算法)及其变种
Twitter开源的分布式ID生成算法,是目前业界最流行、最经典的方案。
核心思想:将一个64位的Long型ID划分为几个部分,每个部分代表不同的信息。
1位符号位:固定为0,表示正数。
41位时间戳:毫秒级的时间差(当前时间 - 自定义起始时间)。可用
(1L << 41) / (1000L * 60 * 60 * 24 * 365) ≈ 69年
。10位工作机器ID(Datacenter ID + Worker ID):最多支持
2^10 = 1024
个节点。12位序列号:同一毫秒内产生的不同ID的序列号,支持每节点每毫秒生成
2^12 = 4096
个ID。
实现方式:本地算法生成,无需中心化服务。需要为每个节点配置唯一的
WorkerID
和DatacenterID
。优点:
高性能:本地生成,无网络开销。
ID趋势递增:按时间戳排序,对数据库友好。
容量巨大:理论上QPS可达400万+。
信息蕴含:ID中包含时间戳和机器信息,可根据ID反推生成时间。
缺点:
时钟回拨问题:如果服务器时钟发生回拨,可能导致生成重复ID。这是最大的挑战。
工作机器ID分配:需要保证每个节点的
WorkerID
不重复,需要额外的管理系统。
解决时钟回拨的方案:
等待时钟同步:当回拨时间很短时(如毫秒级),让服务等待直到时钟追上来。
扩展序列号:当回拨时间较短时,不改变时间戳,而是将序列号的最高位设为1(表示回拨),继续生成ID。
备用ID生成服务:严重回拨时,切换到一个备用的ID生成器。
业界变种:
百度的UidGenerator:基于Snowflake,支持自定义各比特位长度,并提出了通过消费数据库来分配
WorkerID
的方案。美团的Leaf-snowflake:通过ZooKeeper顺序节点来分配
WorkerID
,并解决了时钟回拨问题。
2.2.5 基于Redis生成
利用Redis的单线程原子操作 INCR
或 INCRBY
来生成序列ID。
实现方式:
INCR key
:每次操作将key的值增1,并返回结果。这个操作是原子的。可以结合时间戳:
ID = 时间戳 * 倍数 + INCR的序列号
,以生成趋势递增的ID。
优点:
性能优于数据库:Redis基于内存,性能很高。
ID有序。
缺点:
需要引入和维护Redis,增加了系统复杂性。
存在数据丢失风险:虽然可通过AOF/RDB持久化,但在宕机时仍可能丢失部分序列号,导致ID不连续。
Redis本身需要高可用部署(如哨兵、集群),否则是单点。
适用场景:已经大量使用Redis的环境,对数据连续性要求不极端的场景。
2.2.6 开源中间件(如Tinyid, Leaf)
直接使用成熟的开源解决方案,它们通常是多种方案的集大成者。
Leaf(美团):
Leaf-segment:提供了高可用的号段模式。
Leaf-snowflake:解决了Snowflake的时钟回拨和WorkerID分配问题。
它是一个独立的服务,通过RPC或HTTP提供ID生成服务。
Tinyid(滴滴):
基于号段模式,提供了丰富的RESTful API和客户端。
支持多DB(多活部署),提高了可用性。
优点:
开箱即用,功能完善,经过大厂生产环境验证。
高可用、高性能。
解决了自研可能遇到的坑(如时钟回拨)。
缺点:
需要部署和维护中间件,有一定复杂度。
3、方案对比及技术选型
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
UUID | 简单、无中心、性能极高 | 无序、存储大、不可读 | 临时令牌、会话ID |
数据库多实例 | 基于现有DB、ID有序 | 扩展性差、有数据库压力 | 简单分布式系统,不推荐 |
号段模式 | 高可用、高性能、降低DB压力 | 服务重启ID不连续、有号段粒度 | 大部分业务场景,QPS中等 |
Snowflake | 高性能、趋势递增、容量大 | 时钟回拨、需管理WorkerID | 高并发、对时序有要求 |
Redis | 性能较好、ID有序 | 需维护Redis、有数据丢失风险 | 已有Redis环境 |
开源中间件 | 功能完善、高可用、开箱即用 | 需部署维护中间件 | 大中型公司,追求稳定 |
选型建议:
数据量不大、非核心业务:可以考虑数据库号段模式,实现简单。
追求极致性能、高并发场景(如电商、金融):Snowflake及其变种(如Leaf-snowflake) 是首选。必须解决好时钟回拨和workerId分配问题。
已有Redis/MongoDB集群,且团队熟悉:可以考虑使用它们,但要做好高可用和数据持久化。
完全不想关心基础设施:可以考虑使用云服务商提供的全局唯一ID服务。
4、SpringBoot实现Snowflake算法
4.1 创建Snowflake算法实现类
package com.example.demo.snowflake;
import org.springframework.stereotype.Component;
/**
* Twitter的Snowflake算法实现
* 64位ID (0 + 41位时间戳 + 10位机器ID + 12位序列号)
*/
@Component
public class SnowflakeIdGenerator {
// ==============================Fields===========================================
/** 开始时间戳 (2024-01-01) */
private final long twepoch = 1704067200000L;
/** 机器id所占的位数 */
private final long workerIdBits = 5L;
/** 数据标识id所占的位数 */
private final long datacenterIdBits = 5L;
/** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
private final long maxWorkerId = -1L ^ (-1L maxWorkerId || workerId maxDatacenterId || datacenterId > timestampLeftShift) + twepoch;
long datacenterId = (id >> datacenterIdShift) & maxDatacenterId;
long workerId = (id >> workerIdShift) & maxWorkerId;
long sequence = id & sequenceMask;
return new SnowflakeId(timestamp, datacenterId, workerId, sequence);
}
/**
* Snowflake ID解析结果类
*/
public static class SnowflakeId {
private final long timestamp;
private final long datacenterId;
private final long workerId;
private final long sequence;
public SnowflakeId(long timestamp, long datacenterId, long workerId, long sequence) {
this.timestamp = timestamp;
this.datacenterId = datacenterId;
this.workerId = workerId;
this.sequence = sequence;
}
// getters
public long getTimestamp() { return timestamp; }
public long getDatacenterId() { return datacenterId; }
public long getWorkerId() { return workerId; }
public long getSequence() { return sequence; }
@Override
public String toString() {
return String.format("SnowflakeId{timestamp=%d, datacenterId=%d, workerId=%d, sequence=%d}",
timestamp, datacenterId, workerId, sequence);
}
}
}
4.2 创建配置类
package com.example.demo.config;
import com.example.demo.snowflake.SnowflakeIdGenerator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SnowflakeConfig {
@Value("${snowflake.worker-id:0}")
private long workerId;
@Value("${snowflake.datacenter-id:0}")
private long datacenterId;
@Bean
public SnowflakeIdGenerator snowflakeIdGenerator() {
return new SnowflakeIdGenerator(workerId, datacenterId);
}
}
4.3 配置文件
在 application.yml
或 application.properties
中配置:
# application.yml
snowflake:
worker-id: 1
datacenter-id: 1
4.4 创建Service层
package com.example.demo.service;
import com.example.demo.snowflake.SnowflakeIdGenerator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class IdGeneratorService {
@Autowired
private SnowflakeIdGenerator snowflakeIdGenerator;
/**
* 生成下一个ID
*/
public long generateId() {
return snowflakeIdGenerator.nextId();
}
/**
* 生成字符串格式的ID
*/
public String generateStringId() {
return String.valueOf(snowflakeIdGenerator.nextId());
}
/**
* 解析ID
*/
public SnowflakeIdGenerator.SnowflakeId parseId(long id) {
return snowflakeIdGenerator.parseId(id);
}
}
4.5 创建Controller(可选)
package com.example.demo.controller;
import com.example.demo.service.IdGeneratorService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/id")
public class IdGeneratorController {
@Autowired
private IdGeneratorService idGeneratorService;
@GetMapping("/next")
public Map generateId() {
long id = idGeneratorService.generateId();
Map result = new HashMap<>();
result.put("id", id);
result.put("timestamp", System.currentTimeMillis());
return result;
}
@GetMapping("/parse/{id}")
public Map parseId(@PathVariable long id) {
SnowflakeIdGenerator.SnowflakeId snowflakeId = idGeneratorService.parseId(id);
Map result = new HashMap<>();
result.put("originalId", id);
result.put("timestamp", snowflakeId.getTimestamp());
result.put("datacenterId", snowflakeId.getDatacenterId());
result.put("workerId", snowflakeId.getWorkerId());
result.put("sequence", snowflakeId.getSequence());
return result;
}
@GetMapping("/batch/{count}")
public Map generateBatchIds(@PathVariable int count) {
if (count > 1000) {
throw new IllegalArgumentException("一次性最多生成1000个ID");
}
long[] ids = new long[count];
long startTime = System.currentTimeMillis();
for (int i = 0; i result = new HashMap<>();
result.put("ids", ids);
result.put("count", count);
result.put("timeCost", endTime - startTime + "ms");
return result;
}
}
5、高级实现:解决Snowflake时钟回拨和WorkerID分配问题
5.1 解决时钟回拨:增强的Snowflake实现
package com.example.demo.snowflake.advanced;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* 增强的Snowflake实现,解决时钟回拨问题
*/
@Component
public class EnhancedSnowflakeIdGenerator {
private static final Logger logger = LoggerFactory.getLogger(EnhancedSnowflakeIdGenerator.class);
// ... 之前的常量定义保持不变 ...
private long workerId;
private long datacenterId;
private long sequence = 0L;
private long lastTimestamp = -1L;
// 时钟回拨容忍阈值(毫秒)
private static final long MAX_BACKWARD_MS = 1000L;
// 等待时钟同步的最大重试次数
private static final int MAX_RETRY_COUNT = 3;
public EnhancedSnowflakeIdGenerator(long workerId, long datacenterId) {
// ... 参数校验保持不变 ...
this.workerId = workerId;
this.datacenterId = datacenterId;
}
/**
* 增强的nextId方法,处理时钟回拨
*/
public synchronized long nextId() {
int retryCount = 0;
while (retryCount < MAX_RETRY_COUNT) {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
// 时钟回拨处理
long offset = lastTimestamp - timestamp;
if (offset <= MAX_BACKWARD_MS) {
// 小的回拨,等待时钟追上来
logger.warn("Clock moved backwards by {} milliseconds, waiting...", offset);
try {
TimeUnit.MILLISECONDS.sleep(offset);
timestamp = timeGen();
if (timestamp < lastTimestamp) {
// 等待后仍然回拨,继续重试
retryCount++;
continue;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Thread interrupted while handling clock backward", e);
}
} else {
// 大的回拨,使用备用策略
return handleLargeClockBackward();
}
}
// 正常生成ID的逻辑
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - twepoch) << timestampLeftShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift)
| sequence;
}
throw new RuntimeException("Failed to generate ID after " + MAX_RETRY_COUNT + " retries due to clock issues");
}
/**
* 处理大的时钟回拨
*/
private long handleLargeClockBackward() {
// 策略1:使用扩展序列号(将序列号最高位设为1,表示回拨期间生成的ID)
// 策略2:记录告警,使用备用时间戳
// 这里实现策略2:使用当前时间,但记录严重告警
logger.error("Large clock backward detected! Using system current time with warning flag.");
long currentTimestamp = System.currentTimeMillis();
lastTimestamp = currentTimestamp;
// 在序列号中设置一个特殊标记(使用序列号的最高位)
long warningSequence = sequence | (1L << (sequenceBits - 1));
return ((currentTimestamp - twepoch) << timestampLeftShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift)
| (warningSequence & sequenceMask);
}
// ... 其他方法保持不变 ...
}
5.2 动态WorkerID分配(基于数据库)
创建WorkerID分配表:
CREATE TABLE worker_id_alloc (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
service_name VARCHAR(64) NOT NULL COMMENT '服务名称',
ip_address VARCHAR(64) NOT NULL COMMENT 'IP地址',
port INT NOT NULL COMMENT '端口',
worker_id INT NOT NULL COMMENT '分配的WorkerID',
heartbeat_time DATETIME NOT NULL COMMENT '最后心跳时间',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_service(service_name, ip_address, port),
UNIQUE KEY uk_worker_id(worker_id)
) COMMENT 'WorkerID分配表';
创建WorkerID分配服务:
package com.example.demo.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct;
import java.net.InetAddress;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
@Service
public class WorkerIdAllocService {
@Autowired
private JdbcTemplate jdbcTemplate;
private String serviceName = "order-service"; // 从配置读取
private String ipAddress;
private int port = 8080; // 从配置读取
private Integer allocatedWorkerId;
@PostConstruct
public void init() throws Exception {
// 获取本机IP
this.ipAddress = InetAddress.getLocalHost().getHostAddress();
allocateWorkerId();
}
/**
* 分配WorkerID
*/
@Transactional
public void allocateWorkerId() {
// 检查是否已经分配
String checkSql = "SELECT worker_id FROM worker_id_alloc WHERE service_name = ? AND ip_address = ? AND port = ?";
List> existing = jdbcTemplate.queryForList(checkSql, serviceName, ipAddress, port);
if (!existing.isEmpty()) {
allocatedWorkerId = (Integer) existing.get(0).get("worker_id");
updateHeartbeat();
return;
}
// 查找可用的WorkerID (0-31)
for (int workerId = 0; workerId <= 31; workerId++) {
try {
String insertSql = "INSERT INTO worker_id_alloc(service_name, ip_address, port, worker_id, heartbeat_time) VALUES (?, ?, ?, ?, ?)";
jdbcTemplate.update(insertSql, serviceName, ipAddress, port, workerId, LocalDateTime.now());
allocatedWorkerId = workerId;
logger.info("Successfully allocated WorkerID: {} for {}:{}", workerId, ipAddress, port);
return;
} catch (Exception e) {
// WorkerID已被占用,继续尝试下一个
continue;
}
}
throw new RuntimeException("No available WorkerID found");
}
/**
* 定期发送心跳
*/
@Scheduled(fixedRate = 30000) // 每30秒一次
public void updateHeartbeat() {
if (allocatedWorkerId != null) {
String sql = "UPDATE worker_id_alloc SET heartbeat_time = ? WHERE worker_id = ?";
jdbcTemplate.update(sql, LocalDateTime.now(), allocatedWorkerId);
}
}
/**
* 清理过期的WorkerID分配
*/
@Scheduled(fixedRate = 60000) // 每1分钟一次
public void cleanupExpiredAllocations() {
String sql = "DELETE FROM worker_id_alloc WHERE heartbeat_time < ?";
jdbcTemplate.update(sql, LocalDateTime.now().minusMinutes(5)); // 5分钟未心跳视为过期
}
public Integer getAllocatedWorkerId() {
return allocatedWorkerId;
}
}
5.3 基于ZooKeeper的WorkerID分配
package com.example.demo.service;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.utils.CloseableUtils;
import org.apache.zookeeper.CreateMode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
@Service
public class ZkWorkerIdAllocService {
@Autowired
private CuratorFramework curatorFramework;
private static final String WORKER_ID_PATH = "/snowflake/workers";
private String allocatedPath;
private int workerId;
@PostConstruct
public void init() throws Exception {
// 确保根路径存在
if (curatorFramework.checkExists().forPath(WORKER_ID_PATH) == null) {
curatorFramework.create().creatingParentsIfNeeded().forPath(WORKER_ID_PATH);
}
// 创建临时顺序节点
allocatedPath = curatorFramework.create()
.withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
.forPath(WORKER_ID_PATH + "/worker-");
// 获取workerId(顺序节点的编号)
List children = curatorFramework.getChildren().forPath(WORKER_ID_PATH);
children.sort(String::compareTo);
for (int i = 0; i < children.size(); i++) {
if (("/" + WORKER_ID_PATH + "/" + children.get(i)).equals(allocatedPath)) {
workerId = i % 32; // 限制在0-31范围内
break;
}
}
logger.info("Allocated WorkerID: {} from ZooKeeper", workerId);
}
public int getWorkerId() {
return workerId;
}
@PreDestroy
public void destroy() {
// 节点会自动删除(因为是临时节点)
logger.info("Releasing WorkerID: {}", workerId);
}
}