目录
- 全局唯一ID
- 实现优惠券秒杀下单
- 超卖问题
- 一人一单
- 分布式锁
- Redis优化秒杀
-
全局唯一ID
每个店铺都会发优惠券

用户抢购时,生成的订单保存到tb_voucher_order表,该表没有使用自增ID,因为会产生如下问题: ID规律太明显
用户在我的订单种可以看到编号,暴露信息给用户
- 受表单数据量限制
因为订单数据过多,数据保存到多张表,如果采用自增长,会出现ID重复的情况
所以引出概念:全局ID生成器
即一种在分布式系统下生成全局唯一ID的工具,一般满足特性;
- 唯一性
- 高可用
- 高性能
- 递增性
- 安全性
Redis实现全局唯一ID
全局唯一ID生成策略:
- UUID
- Redis自增
- snowflake算法
- 数据库自增
Redis自增ID策略
每天一个key,方便统计订单量,一天的订单量上限2^32位
//基于Redis的ID生成器@Componentpublic class RedisIdWorker {//在main函数里生成的2022年1月1日0点0分的秒数private static final long BEGIN_TIMESTAMP = 1640995200L;private static final int COUNT_BITS = 32;private StringRedisTemplate stringRedisTemplate;public long nextId(String KeyPrefix){//1.生成时间戳LocalDateTime now = LocalDateTime.now();long nowSecond = now.toEpochSecond(ZoneOffset.UTC);long timestamp = nowSecond - BEGIN_TIMESTAMP;//2.生成序列号//2.1获取当前日期,精准到天String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));//2.2自增长Long count = stringRedisTemplate.opsForValue().increment("icr:" + KeyPrefix + ":" + date);//3.拼接并返回(时间戳左移32位,再用或运算填充)return timestamp << COUNT_BITS | count;}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);}}
测试并发生成ID,这里多线程玩的有点酷- -
@Resourceprivate RedisIdWorker redisIdWorker;private ExecutorService es = Executors.newFixedThreadPool(500);@Testvoid 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));}
测试成功,redis中存在数据
注意:此处如果一直转圈,可能是忘记注入Redis,需要使用构造函数

添加优惠券

有两种不同的券,秒杀券(有时间要求)需要抢购。
数据库中tb_seckill_voucher表为空,需要新增秒杀券
使用Postman进行添加
POST:http://localhost:8081/voucher/seckill{"shopId": 1,"title": "100原代金券","subTitle": "周一至周五可使用","rules": "全场通用","payValue": 8000,"actualValue": 10000,"type": 1,"stock": 100,"beginTime": "2022-05-12T22:00:00","endTime": "2022-05-12T23:00:00"}
添加成功后在数据库中看到新增记录

注意:如果数据库有数据但优惠券没有显示,查看当前时间是否正在指定时间范围。
修改时间后,成功显示。
实现秒杀下单

下单需要判断:秒杀是否开始或结束
- 库存是否充足

@Servicepublic class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Resourceprivate ISeckillVoucherService seckillVoucherService;@Resourceprivate RedisIdWorker redisIdWorker;@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();//6.1订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);//6.2用户idLong userId = UserHolder.getUser().getId();voucherOrder.setUserId(userId);//6.3代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);//7.返回订单idreturn Result.ok(orderId);}}
⭐超卖问题
即并发问题,
情景:通过JMeter做高并发测试,100张优惠券卖出去变成负数,生成了两百来个订单
解决方案:加锁
乐观锁!
方法一:版本号法
方法二:CAS法
乐观锁解决超卖



一人一单

//一人一单Long userId = UserHolder.getUser().getId();Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();if(count > 0){//用户已经购买过return Result.fail("用户已经购买过!");}
问题依然存在,一个用户下了10单,同样是并发问题
(因为是新增数据,无法判断是否修改过,不能使用乐观锁)
@Overridepublic 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("库存不足!");}//7.返回订单idreturn createVoucherOrder(voucherId);}@Transactionalpublic Result createVoucherOrder(Long voucherId) {//一人一单Long userId = UserHolder.getUser().getId();synchronized (userId.toString().intern()){}Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();if(count > 0){//用户已经购买过return Result.fail("用户已经购买过!");}//5.扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).gt("stock",0).update();if(!success) {return Result.fail("库存不足!");}//6.创建订单VoucherOrder voucherOrder = new VoucherOrder();//6.1订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);//6.2用户idvoucherOrder.setUserId(userId);//6.3代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);//7.返回订单idreturn Result.ok(orderId);}
⭐一人一单并发安全问题


集群模式下,有多个JVM存在,每个JVM内部都有自己的锁,导致每个锁都可以有一个线程获取,变成并行运行。
