黑马Redis项目笔记:秒杀 全局唯一ID

254 阅读2分钟

问题

每个店铺都可以发布优惠券,当用户抢购时,就会生成订单并保存到tb_voucher_order这张表中。

而订单如果使用数据库的自增id就会存在问题。

  • 自增id太明显
  • 受到单标数量的限制

全局ID生成器

全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性:

  1. 唯一性
  2. 高可用
  3. 高性能
  4. 递增性
  5. 安全性

image.png

为了增加ID的安全性(减少ID的规律性),我们不可以直接使用Redis直接自增的数值,而是拼接一些其他的信息:

image.png

ID的组成部分:

  1. 符号位,1bit,永远为0
  2. 时间戳,31bit,以秒为单位,能用69年。
  3. 序列号,秒以内的计算器,支持每秒产生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);

运行后可以看到

image.png

使用当前时间减去开始时间就可以得到时间戳

//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();

执行结束后结果如下

image.png

选取其中一个id转换成2进制

image.png

如图,符合上面的要求,再用一个来进行查看

image.png

看最后4位,符合结果,自增了1.