• Redis实战案例及问题分析之单机优惠券秒杀


    目录

    全局唯一ID

    全局ID生成器

     实现优惠券秒杀的下单功能

    优惠券秒杀超卖问题

     乐观锁解决秒杀券超卖问题

    一人一单


    全局唯一ID

    当用户抢购时,就会生成订单并保存到tb_voucher_order这张表中,而订单表如果使用数据库自增ID就存在如下问题:

    • id的规律太明显:容易被用户推测出某些信息
    • 受单表数据量的限制:当优惠券订单数量过多,就要彩标存储,这时不同的数据表中会出现

    全局ID生成器

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

    • 唯一性
    • 高可用(任何时候来都可用)
    • 高性能
    • 递增性(整体单调增)
    • 安全性

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

    1. @Component
    2. public class RedisIdWorker {
    3. private StringRedisTemplate stringRedisTemplate;
    4. public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
    5. this.stringRedisTemplate = stringRedisTemplate;
    6. }
    7. /**
    8. * 开始时间戳
    9. */
    10. private static final long BEGIN_TIMESTAMP = 1640995200L;
    11. /**
    12. * 序列号位数
    13. */
    14. private static final int COUNT_BITS = 32;
    15. public long nextID(String keyPrefix){
    16. //1.生成时间戳
    17. LocalDateTime now = LocalDateTime.now();
    18. long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
    19. long timestamp = nowSecond - BEGIN_TIMESTAMP;
    20. //2.生成序列号
    21. //2.1获取当前日期,精确到天
    22. String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
    23. //2.2自增长
    24. long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
    25. //3.拼接并返回
    26. return timestamp<<COUNT_BITS | count;
    27. }
    28. }

     实现优惠券秒杀的下单功能

    下单需要判断两点:

    • 秒杀是否开始或结束。如果尚未开始或已经结束则无法下单
    • 库存是否充足 ,不足则无法下单

     

    1. @Service
    2. public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
    3. @Resource
    4. private ISeckillVoucherService seckillVoucherService;
    5. @Resource
    6. private RedisIdWorker redisIdWorker;
    7. @Override
    8. @Transactional
    9. public Result seckillVoucher(Long voucherId) {
    10. //1.查询优惠券,去秒杀优惠券的库存去查
    11. SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
    12. //2.查询开始时间
    13. if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
    14. //如果还没开始
    15. return Result.fail("秒杀还没开始");
    16. }
    17. //3.查询结束时间
    18. if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
    19. return Result.fail("秒杀已经结束");
    20. }
    21. //4.判断库存是否充足
    22. if (voucher.getStock()<1) {
    23. return Result.fail("优惠券已经被抢完");
    24. }
    25. //5.扣减库存
    26. boolean success = seckillVoucherService.update().setSql("stock = stock-1").eq("voucher_id", voucherId).update();
    27. if (!success) {
    28. //扣减失败
    29. return Result.fail("优惠券库存不足");
    30. }
    31. //6.创建订单
    32. VoucherOrder voucherOrder = new VoucherOrder();
    33. //6.1订单ID
    34. long orderId = redisIdWorker.nextID("order:");
    35. voucherOrder.setId(orderId);
    36. //6.2用户id
    37. Long userId = UserHolder.getUser().getId();
    38. voucherOrder.setUserId(userId);
    39. //6.3代金券id
    40. voucherOrder.setVoucherId(voucherId);
    41. save(voucherOrder);
    42. //7.返回订单
    43. return Result.ok(orderId);
    44. }
    45. }

    优惠券秒杀超卖问题

    为什么会出现超卖问题?高并发的情况下,会出现访问量过大,同时拿到了库存大于1的数据,多个线程交叉执行,就会出现超卖问题。

    超卖问题是典型的多线程安全问题,针对这一个问题的常见解决方案是加锁:

     悲观锁性能较低,这里使用乐观锁。

    乐观锁的关键是判断之前查询得到的数据是否有被修改过,常见的方式有两种:

    版本号发:

     CAS法(先比较后设置),用库存代替版本号

     乐观锁解决秒杀券超卖问题

    乐观锁解决超卖问题存在的弊端:成功率低。因为只要判断库存不相等就执行失败,当高并发执行的时候会有大多的线程执行失败。

    解决办法:不再是判断库存是否相等,而是去判断库存是否大于1。

    1. //5.扣减库存
    2. boolean success = seckillVoucherService.update()
    3. .setSql("stock = stock-1")//set stock = stock - 1
    4. .eq("voucher_id", voucherId).gt("stock",0) //where id = ? and stock >0
    5. .update();

    一人一单

    需求:要求同一个优惠券,一个用户只能下一单。

     为了解决并发带来的线程不安全问题,这里采用的是悲观锁,也就是加了synchronized关键字。

    1. public Result seckillVoucher(Long voucherId) {
    2. //1.查询优惠券,去秒杀优惠券的库存去查
    3. SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
    4. //2.查询开始时间
    5. if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
    6. //如果还没开始
    7. return Result.fail("秒杀还没开始");
    8. }
    9. //3.查询结束时间
    10. if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
    11. return Result.fail("秒杀已经结束");
    12. }
    13. //4.判断库存是否充足
    14. if (voucher.getStock()<1) {
    15. return Result.fail("优惠券已经被抢完");
    16. }
    17. Long userId = UserHolder.getUser().getId();
    18. synchronized(userId.toString().intern()){
    19. IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
    20. return proxy.creatVoucherOrder(voucherId);
    21. }
    22. }
    23. @Transactional
    24. public Result creatVoucherOrder(Long voucherId) {
    25. //5.一人一单
    26. //5.1查询用户
    27. Long userId = UserHolder.getUser().getId();
    28. //5.2查询订单
    29. int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
    30. //5.3判断用户是否存在
    31. if (count > 0) {
    32. //用户已经购买过了
    33. return Result.fail("用户已经购买过了");
    34. }
    35. //6.扣减库存
    36. boolean success = seckillVoucherService.update()
    37. .setSql("stock = stock-1")//set stock = stock - 1
    38. .eq("voucher_id", voucherId).gt("stock",0) //where id = ? and stock >0
    39. .update();
    40. if (!success) {
    41. //扣减失败
    42. return Result.fail("优惠券库存不足");
    43. }
    44. //7.创建订单
    45. VoucherOrder voucherOrder = new VoucherOrder();
    46. //7.1订单ID
    47. long orderId = redisIdWorker.nextID("order:");
    48. voucherOrder.setId(orderId);
    49. voucherOrder.setUserId(userId);
    50. //7.3代金券id
    51. voucherOrder.setVoucherId(voucherId);
    52. save(voucherOrder);
    53. //8.返回订单
    54. return Result.ok(orderId);
    55. }
    56. }

  • 相关阅读:
    你的工具包已到货「GitHub 热点速览 v.22.31」
    57. 插入区间
    gorm的使用(持续更新)
    23端口登录的Telnet命令+传输协议FTP命令
    抖音实战~发布短视频流程梳理
    优橙内推海南专场——5G网络优化(中高级)工程师
    2023最新SSM计算机毕业设计选题大全(附源码+LW)之java我的课堂642o1
    雷军:我的程序人生路
    excel自动翻译-excel一键自动翻译免费
    CMMI认证要求
  • 原文地址:https://blog.csdn.net/PnJgHT/article/details/125491875