分布式ID
分布式ID:distributed id,在分布式系统中生成的全局唯一标识符。
使用场景:订单号、分库分表环境下的数据库主键等
分布式ID常见的实现方式:
- UUID:例如,
UUID.randomUUID().toString(),优点是本地生成,无网络开销,缺点是生成的id太大,导致存储空间大,查询效率低,不适合作为数据库主键 - 号段模式:每次获取一个号段,然后在内存中分配这个号段,分配完成后再次从数据库中获取。 优点是可以减少数据库的访问
- 雪花算法:snow flake algorithm,由推特开源的分布式ID生成算法,生成一个64位的ID,它的结构是 1位符号位 + 41位时间戳 + 10位机器ID + 12位序列号。它的优点是本地生成、性能高、趋势递增,缺点是依赖机器时钟,如果时钟回拨,可能会生成重复的ID
- 基于redis的原子操作:基于redis提供的操作,incr、incrby,来生成分布式id,优点是性能较好,id可以保证全局递增,缺点是每次生成都依赖redis
在实际生产中,雪花算法估计是使用最多的,虽然它依赖机器时钟,但生产环境下通常不会出现时钟不准、时钟回拨等情况,推荐使用ntp服务来维护机器时间
雪花算法
雪花算法的原理:
1、雪花算法使用41比特位来存储时间戳,使用如下案例来计算,41比特位可以存储的时间戳,在约80年左右后才会满
java
@Test
public void test1() {
long limit = (1L << 42) - 1;
long sub = limit - System.currentTimeMillis(); // 当前2025年
System.out.println("支持" + (sub / 1000 / 60 / 60 / 24 / 365) + "年"); // 83
}
2、雪花算法使用10比特位来存储机器id,这支持1024个机器id,通常,单个服务最多不会部署超过100台机器,尤其是在分布式、微服务的背景下,所以1024个机器预计完全够用。
3、雪花算法使用12比特位序列号,也就是说,1毫秒内,支持生成4096个序列号,在单台机器上,预计完全够用。
雪花算法的实现
java
/**
* 使用雪花算法生成分布式id,线程安全
*/
public class SnowFlakeIdGenerator implements IdGenerator {
/**
* 时间戳的位数,二进制的
*/
private static final int TS_BIT_SIZE = 41;
/**
* 机器id的位数
*/
private static final int WORKER_BIT_SIZE = 10;
/**
* 序列号的位数
*/
private static final int SEQUENCE_BIT_SIZE = 12;
/**
* workId的最大值
*/
private static final long MAX_WORK_ID = (1L << WORKER_BIT_SIZE) - 1;
/**
* 序列号的最大值
*/
private static final int MAX_SEQUENCE_ID = (1 << SEQUENCE_BIT_SIZE) - 1;
/**
* 基础时间,用于减少分布式ID的大小
*/
private static final long BASE_TIME = getBaseTime();
/**
* 机器id
*/
private final long workId;
/**
* 序列号,在同一毫秒内递增
*/
private int sequence;
/**
* 上一次生成分布式ID时的时间戳
*/
private long lastTime;
private final ReentrantLock LOCK = new ReentrantLock();
/* 提供两个构造方法,1个使用默认方式获取workId,一个由外部传入workId */
public SnowFlakeIdGenerator() {
workId = defaultWorkId();
}
public SnowFlakeIdGenerator(Long workId) {
if (workId == null || workId < 0 || workId > MAX_WORK_ID) {
throw new RuntimeException("workId不符合规范");
}
this.workId = workId;
}
@Override
public long generateId() {
LOCK.lock();
try {
long time = System.currentTimeMillis();
if (time < lastTime) {
throw new RuntimeException("发生时钟回拨");
}
if (lastTime == time) {
if (sequence == MAX_SEQUENCE_ID) {
time = waitNextTime(time);
lastTime = time;
sequence = 0;
} else {
sequence++;
}
} else {
lastTime = time;
sequence = 0;
}
return ((time - BASE_TIME) << (SEQUENCE_BIT_SIZE + WORKER_BIT_SIZE))
| (workId << SEQUENCE_BIT_SIZE)
| sequence;
} finally {
LOCK.unlock();
}
}
private long waitNextTime(final long time) {
long t = System.currentTimeMillis();
while (t <= time) {
t = System.currentTimeMillis();
}
return t;
}
/**
* 根据IP地址,获取默认的workId
*/
private Long defaultWorkId() {
// 根据ip地址生成一个workId
try {
String address = getLocalIp();
long currentPid = getCurrentPid();
return (address.hashCode() + currentPid) * 31 % (MAX_WORK_ID + 1);
} catch (UnknownHostException e) {
e.printStackTrace();
throw new RuntimeException("机器ID生成失败");
}
}
/**
* 获取本机的IP地址
*/
private String getLocalIp() throws UnknownHostException {
return InetAddress.getLocalHost().getHostAddress();
}
/**
* 获取当前进程ID
* @return 当前进程ID,获取失败返回-1
*/
private long getCurrentPid() {
try {
String runtimeName = ManagementFactory.getRuntimeMXBean().getName();
if (runtimeName == null || !runtimeName.contains("@")) {
return -1;
}
return Long.parseLong(runtimeName.split("@")[0]);
} catch (Exception e) {
// 捕获解析失败/空指针等异常
System.err.println("获取PID失败:" + e.getMessage());
return -1;
}
}
private static long getBaseTime() {
final String S = "2016-01-01 00:00:00";
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date;
try {
date = formatter.parse(S);
} catch (ParseException e) {
e.printStackTrace();
throw new RuntimeException("解析基础时间失败");
}
return date.getTime();
}
/**
* 解析ID获取详细信息(调试用)
*/
public Data parseId(long id) {
long timestamp = (id >> (SEQUENCE_BIT_SIZE + WORKER_BIT_SIZE)) + BASE_TIME;
long workerId = (id >> SEQUENCE_BIT_SIZE) & MAX_WORK_ID;
long sequence = id & MAX_SEQUENCE_ID;
return new Data(timestamp, workerId, sequence);
}
public static class Data {
public long timestamp;
public long workerId;
public long sequence;
public Data(long timestamp, long workerId, long sequence) {
this.timestamp = timestamp;
this.workerId = workerId;
this.sequence = sequence;
}
}
}
测试:
java
public class SnowFlakeIdGeneratorTest {
/**
* 测试,验证生成的序列化id,后一个一定大于前一个。单线程
*/
@Test
public void test1() {
IdGenerator idGenerator = new SnowFlakeIdGenerator();
long lastId = 0;
for (int i = 0; i < 300000; i++) {
long id = idGenerator.generateId();
if (id <= lastId) {
System.out.println(lastId + " " + id);
throw new RuntimeException("id重复");
}
lastId = id;
}
}
/**
* 测试,验证生成的序列化id,后一个一定大于前一个。多线程
*/
@Test
public void test2() {
IdGenerator idGenerator = new SnowFlakeIdGenerator(10L);
List<List<Long>> lists = new ArrayList<>();
for (int i = 0; i < 10; i++) {
lists.add(new ArrayList<>());
}
List<Thread> threadList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
int finalI = i;
threadList.add(new Thread(() -> {
List<Long> list = lists.get(finalI);
long lastId = 0;
for (int j = 0; j < 30000; j++) {
long id = idGenerator.generateId();
list.add(id);
if (id <= lastId) {
System.out.println(lastId + " " + id);
throw new RuntimeException("id重复");
}
lastId = id;
}
}));
}
for (Thread thread : threadList) {
thread.start();
}
for (Thread thread : threadList) {
try {
thread.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
Set<Long> set = new HashSet<>();
for (List<Long> list : lists) {
for (Long id : list) {
if (set.contains(id)) {
throw new RuntimeException("重复id");
} else {
set.add(id);
}
}
}
ArrayList<Long> list = new ArrayList<>(set);
list.sort(Long::compareTo);
int i = 0;
for (Long l : list) {
SnowFlakeIdGenerator snowFlakeIdGenerator = new SnowFlakeIdGenerator();
SnowFlakeIdGenerator.Data data = snowFlakeIdGenerator.parseId(l);
System.out.println(l + " " +
"data.timestamp = " + data.timestamp + " " + formatter.format(new Date(data.timestamp)) + " " +
"data.workerId = " + data.workerId + " " +
"data.sequence = " + data.sequence);
i++;
if (i == 20000) {
break;
}
}
}
@Test
public void test3() {
System.out.println(Long.MAX_VALUE);
}
}
性能测试:
java
@State(Scope.Benchmark)
@BenchmarkMode(Mode.Throughput) // 测试吞吐量
@OutputTimeUnit(TimeUnit.SECONDS)
@Warmup(iterations = 3, time = 1) // 预热3轮,每轮1秒
@Measurement(iterations = 5, time = 2) // 测试5轮,每轮2秒
@Threads(100) // 100个线程并发
@Fork(1)
public class SnowflakeJMHTest {
private IdGenerator idGenerator;
private ConcurrentHashMap<Long, Boolean> idSet;
private AtomicLong duplicateCount;
@Setup
public void setup() {
idGenerator = new SnowFlakeIdGenerator(10L);
idSet = new ConcurrentHashMap<>();
duplicateCount = new AtomicLong(0);
}
@Benchmark
public void testConcurrentGeneration() {
long id = idGenerator.generateId();
// 检查重复
Boolean existed = idSet.putIfAbsent(id, Boolean.TRUE);
if (existed != null) {
duplicateCount.incrementAndGet();
}
}
@TearDown
public void tearDown() {
long duplicates = duplicateCount.get();
if (duplicates > 0) {
System.err.println("警告:发现 " + duplicates + " 个重复ID!");
}
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(SnowflakeJMHTest.class.getSimpleName())
.build();
new Runner(opt).run();
}
}
测试结果:
text
Benchmark Mode Cnt Score Error Units
SnowflakeJMHTest.testConcurrentGeneration sample 12648265 3859.767 ± 69.740 ns/op
SnowflakeJMHTest.testConcurrentGeneration:p0.00 sample ≈ 0 ns/op
SnowflakeJMHTest.testConcurrentGeneration:p0.50 sample ≈ 0 ns/op
SnowflakeJMHTest.testConcurrentGeneration:p0.90 sample 100.000 ns/op
SnowflakeJMHTest.testConcurrentGeneration:p0.95 sample 100.000 ns/op
SnowflakeJMHTest.testConcurrentGeneration:p0.99 sample 1200.000 ns/op
SnowflakeJMHTest.testConcurrentGeneration:p0.999 sample 971776.000 ns/op
SnowflakeJMHTest.testConcurrentGeneration:p0.9999 sample 2025472.000 ns/op
SnowflakeJMHTest.testConcurrentGeneration:p1.00 sample 30212096.000 ns/op
性能统计分析,最快的,0秒就可以返回,最慢的,要30毫秒,TP99在9微妙左右。
雪花算法相关问题
时钟回拨问题
时钟回拨:系统时间向后跳变,例如NTP服务同步时间,或者手动设置时间。据统计,在使用ntp服务的情况下,物理服务器大约每月会发生0到3次轻微时钟回拨,回拨范围在100ms以内。(来自deepseek的数据)
如何处理时钟回拨:这里的方案是抛异常,业界成熟的方案:轻微时钟回拨,通常是200毫秒以内,等待,其它程度的时间回拨,抛异常。
workId重复问题
当前代码中,使用IP地址的哈希值 + 进程号,然后和1024取模,来生成workId,有小概率情况下,会发生哈希冲突,想要避免这种情况,可以使用redis,来为当前进程分配一个唯一id,这个id记录在redis中,不过这种方式存在回收id比较困难的情况,可以写一个扫描程序,检测指定服务器上是否有该进程id,如果没有,回收workId。