1. 完整可运行代码
package com.hmdp.utils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
/**
* Redis分布式ID生成器(黑马点评原版)
* 功能:生成全局唯一、有序的分布式ID(订单ID、用户ID等)
*/
@Component
public class RedisIdWorker {
/**
* 开始时间戳:2022-01-01 00:00:00 UTC对应的秒数
* 运行main方法可验证:1640995200L
*/
private static final long BEGIN_TIMESTAMP = 1640995200L;
/**
* 序列号的位数:32位
* 对应ID结构中,低32位存自增序号,高31位存时间戳,最高1位符号位固定为0
*/
private static final int COUNT_BITS = 32;
// 注入Redis操作模板
private StringRedisTemplate stringRedisTemplate;
// 构造方法注入
public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 核心方法:生成下一个全局唯一ID
* @param keyPrefix 业务前缀(比如order、user,区分不同业务的ID)
* @return 64位long型全局唯一ID
*/
public long nextId(String keyPrefix) {
// 1. 生成时间戳
LocalDateTime now = LocalDateTime.now();
// 获取当前时间的UTC秒数(从1970-01-01 00:00:00开始的秒数)
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
// 计算相对时间戳:当前秒数 - 基准时间(2022-01-01)
long timestamp = nowSecond - BEGIN_TIMESTAMP;
// 2. 生成序列号
// 2.1 获取当前日期,精确到天(格式:yyyy:MM:dd)
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
// 2.2 自增长:调用Redis的INCR命令,原子自增,保证序号不重复
// Redis的key格式:icr:{业务前缀}:{日期},比如icr:order:2022:01:25
long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
// 3. 拼接并返回最终ID
// 时间戳左移32位(空出低32位给序号),然后按位或序号,完成拼接
return timestamp << COUNT_BITS | count;
}
/**
* 测试方法:计算2022-01-01 00:00:00 UTC对应的秒数
* 运行结果:second = 1640995200
*/
public static void main(String[] args) {
LocalDateTime time = LocalDateTime.of(2022, 1, 1, 0, 0, 0);
long second = time.toEpochSecond(ZoneOffset.UTC);
System.out.println("second = " + second);
}
}
2. 测试代码
package com.hmdp;
import com.hmdp.utils.RedisIdWorker;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@SpringBootTest
public class HmDianPingApplicationTests {
// 注入ID生成器
@Resource
private RedisIdWorker redisIdWorker;
// 创建500个线程的线程池,模拟高并发场景
private ExecutorService es = Executors.newFixedThreadPool(500);
@Test
void testIdWorker() throws InterruptedException {
// 倒计时锁:300个任务,等所有任务执行完再结束测试
CountDownLatch latch = new CountDownLatch(300);
// 定义任务:每个线程循环100次,生成ID
Runnable task = () -> {
for (int i = 0; i < 100; i++) {
long id = redisIdWorker.nextId("order");
System.out.println("id = " + id);
}
// 任务完成,倒计时-1
latch.countDown();
};
// 记录开始时间
long begin = System.currentTimeMillis();
// 提交300个任务到线程池
for (int i = 0; i < 300; i++) {
es.submit(task);
}
// 等待所有任务执行完成
latch.await();
// 记录结束时间,计算总耗时
long end = System.currentTimeMillis();
System.out.println("time = " + (end - begin));
}
}
3. 核心常量解释
|-----------------|-------------|-------|----------------------------------------------|
| 常量名 | 值 | 作用 | 解释 |
| BEGIN_TIMESTAMP | 1640995200L | 基准时间戳 | 2022 年 1 月 1 日 0 点的 UTC 秒数,作为时间计算的起点,避免时间戳过大 |
| COUNT_BITS | 32 | 序列号位数 | 给自增序号分配 32 个二进制位,时间戳占剩下的 31 位,最高 1 位符号位固定为 0 |
4.ID 结构设计
最终生成的是一个 64 位的 long 型 ID,结构如下:
|-----------|------------|---------------|
| 符号位(1bit) | 时间戳(31bit) | 序列号(32bit) |
| 固定为 0(正数) | 相对基准时间的秒数 | Redis 原子自增的序号 |
各部分作用
符号位:1bit,永远是 0,保证 ID 是正数,符合 Java long 类型的规范
时间戳 :31bit,用「当前秒数 - 基准秒数」计算,最多可以用
2^31 / (365*24*3600) ≈ 68年,足够业务使用序列号 :32bit,用 Redis 的
INCR命令原子自增,同一秒内、同一业务、同一天 的序号绝对不重复,最多支持2^32 = 42亿个 ID / 天
5.测试代码讲解
测试用例,是模拟高并发场景:
(1)Executors.newFixedThreadPool(500):创建 500 个线程的线程池
(2)CountDownLatch(300):倒计时锁,保证 300 个任务全部执行完再结束测试
(3)每个任务循环 100 次,总共生成 300*100=30000 个 ID
(4)运行结果:3 秒左右生成 3 万个 ID,性能极高,且所有 ID绝对不重复