永远要记得坚持的意义
概念: 以订单表的 id 为例
使用自增 id 会产生的问题:
因此,我们需要定义全局 id
全局 id 生成器的特性:
分布式系统下用来生成全局唯一 id 的工具,其特性有:
全局唯一 id 生成的实现:
我们本文主要介绍 使用 Redis 作为全局唯一 id 生成器
我们不直接使用 redis 自增的数值,而是拼接一些其他信息,主要拼接的信息如下:
符号位 + 时间戳 + 序列号
封装好的 Redis 生成唯一 id 的工具类如下:
@Component
public class RedisIdWorker {private static final long BEGIN_TIME = 1670198400;private StringRedisTemplate stringRedisTemplate;private RedisIdWorker(StringRedisTemplate stringRedisTemplate){this.stringRedisTemplate = stringRedisTemplate;}public long nextId(String keyPrefix){// 1. 生成时间戳LocalDateTime localDateTime = LocalDateTime.now();long nowSecond = localDateTime.toEpochSecond(ZoneOffset.UTC);long timestamp = nowSecond - BEGIN_TIME;// 2. 生成序列号 —— 防止超过上限(Redis 自增的上限 2 的64 次幂) 解决方式:再拼接一个日期字符串// 2.1 获取当前日期String day = localDateTime.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + day);// 3. 拼接并返回return timestamp<<32 | count; // 前 32 位为时间戳 ,后 32 位为 序列号(即 count)}// 生成时间戳开始时间 —— 我这里是 2022 / 12/ 5 日开始
// public static void main(String[] args) {
// LocalDateTime time = LocalDateTime.of(2022, 12, 5, 0, 0, 0);
// long second = time.toEpochSecond(ZoneOffset.UTC);
// System.out.println(second);
// }
}
测试类代码:
@Slf4j
@SpringBootTest
class HmDianPingApplicationTests {@Autowiredprivate RedisIdWorker redisIdWorker;// 500 线程的线程池子private ExecutorService es = Executors.newFixedThreadPool(500);@Testpublic void testIdWorker() throws InterruptedException {CountDownLatch latch = new CountDownLatch(200);// 每个线程生成 100 个 idRunnable task = () -> {for (int i=0; i<5; i++) {long id = redisIdWorker.nextId("order");log.info("id=" + id);}latch.countDown();};long begin = System.currentTimeMillis();for (int i=0; i<200; i++){es.submit(task);}latch.await();long end = System.currentTimeMillis();log.info("time=" + (end - begin));}}
使用全局唯一 id 解决秒杀业务
@Resourceprivate RedisIdWorker redisIdWorker;@Resourceprivate ISeckillVoucherService seckillVoucherService;@Override@Transactionalpublic Result seckillVoucher(Long voucherId) {// 1. 查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2. 判断秒杀是否开始if(voucher.getBeginTime().isAfter(LocalDateTime.now())){return Result.fail("秒杀还未开始");}// 3. 判断秒杀是否已经结束if(voucher.getEndTime().isBefore(LocalDateTime.now())){return Result.fail("秒杀已经结束");}// 4. 判断库存是否充足if(voucher.getStock() < 1){return Result.fail("库存不足");}// 5. 扣除库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).update();if (!success){return Result.fail("库存不足");}// 6. 创建订单VoucherOrder voucherOrder = new VoucherOrder();// 订单的参数 —— 订单 id \ 代金券 id \ 用户 id// 生成唯一 idlong id = redisIdWorker.nextId("order");voucherOrder.setId(id);voucherOrder.setVoucherId(voucherId);voucherOrder.setUserId(1L);save(voucherOrder);return Result.ok();}