问题
每个店铺都可以发布优惠券,当用户抢购时,就会生成订单并保存到tb_voucher_order这张表中。
而订单如果使用数据库的自增id就会存在问题。
- 自增id太明显
- 受到单标数量的限制
全局ID生成器
全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性:
- 唯一性
- 高可用
- 高性能
- 递增性
- 安全性
为了增加ID的安全性(减少ID的规律性),我们不可以直接使用Redis直接自增的数值,而是拼接一些其他的信息:
ID的组成部分:
- 符号位,1bit,永远为0
- 时间戳,31bit,以秒为单位,能用69年。
- 序列号,秒以内的计算器,支持每秒产生2的32次方个不同的ID
保证了以上五个性质,时间相同的情况下序列号不同满足了唯一性。且时间和序列号都是自增的,满足了递增性。 且规律变得复杂满足了安全性。
代码实现
这里首先贴上全部代码
@Component
public class RedisIdWorker {
/**
* 开始时间戳
*/
private static final Long BEGIN_TIMESTAMP = 1672531200L;
private static final int COUNT_BITS =32;
private StringRedisTemplate stringRedisTemplate;
public RedisIdWorker(StringRedisTemplate stringRedisTemplate){
this.stringRedisTemplate = stringRedisTemplate;
}
public long nextId(String keyPrefix){
//1生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowSecond - BEGIN_TIMESTAMP;
//2生成序列号
//获取当前日期,精确到天
String date = now.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":"+date);
//3拼接并且返回
return timestamp << COUNT_BITS | count ;
}
}
生成时间戳
首先设定开始时间戳
/**
* 开始时间戳
*/
private static final Long BEGIN_TIMESTAMP = 1672531200L;
取得开始时间戳的方法
LocalDateTime time = LocalDateTime.of(2022, 1, 1, 0, 0, 0);
long second = time.toEpochSecond(ZoneOffset.UTC);
System.out.println("second="+second);
运行后可以看到
使用当前时间减去开始时间就可以得到时间戳
//1生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowSecond - BEGIN_TIMESTAMP;
生成序列号
//2生成序列号
//获取当前日期,精确到天
String date = now.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
//自增长
long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":"+date);
拼接返回
首先把时间戳位运算让出32位,再用或运算拼接count位置
private static final int COUNT_BITS =32;
//3拼接并且返回
return timestamp << COUNT_BITS | count ;
测试方法
编写一个测试方法测试一下代码
@Resource
private RedisIdWorker redisIdWorker;
private ExecutorService es = Executors.newFixedThreadPool(500);
@Test
void testIdWorker() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(300);
Runnable task = () ->{
for (int i = 0; i < 100; i++) {
long id = redisIdWorker.nextId("order");
System.out.println("id="+id);
}
latch.countDown();
};
long begin = System.currentTimeMillis();
for (int i = 0; i < 300; i++) {
es.submit(task);
}
latch.await();
long end = System.currentTimeMillis();
System.out.println("time:"+(end - begin));
}
简单讲讲countDownLatch是什么,countDownLatch是发令枪的意思,当分线程执行完一个异步任务后,则执行一次countDown方法告诉主线程该部分任务已经执行完毕。当所有任务执行完毕后,调用了await()方法而受到阻塞的主线程的将会返回继续运行。
这里记录一下在之前一个不太理解的多线程运行的点
首先初始化了一个有500个线程的线程池
private ExecutorService es = Executors.newFixedThreadPool(500);
创建一个Task,里面是调用了ID生成器的任务
Runnable task = () ->{
for (int i = 0; i < 100; i++) {
long id = redisIdWorker.nextId("order");
System.out.println("id="+id);
}
latch.countDown();
};
当主线程进入循环的时候,执行submit方法,则立刻会有一个分线程来执行上面的任务,此时主线程继续执行循环,总共执行300次。当主线程执行完for循环时,执行await方法,子线程继续执行submit上去的task直到所有子线程的任务执行完毕并且主线程返回。记录时间。
for (int i = 0; i < 300; i++) {
es.submit(task);
}
latch.await();
执行结束后结果如下
选取其中一个id转换成2进制
如图,符合上面的要求,再用一个来进行查看
看最后4位,符合结果,自增了1.