• Redis的优惠券秒杀问题(五)全局唯一ID 以及 秒杀下单


    Redis的优惠券秒杀问题(五)全局唯一ID 以及 秒杀下单

    关于优惠秒杀问题的Redis实现章节总览

    全局唯一ID 

    场景分析 

    不能用自增的原因

    id的规律性太明显

    受单表数据量的限制

    全局唯一ID的条件

    全局唯一ID的Redis实现

    代码实现

    单元测试 

    其它全局唯一ID的生成策略 

    秒杀下单 

    场景分析 

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

    代码实现 

    存在问题


    Redis的优惠券秒杀问题(五)全局唯一ID 以及 秒杀下单

    关于优惠秒杀问题的Redis实现章节总览

    我们要讲述的问题大致如下所示,根据黑马程序员视频教程,会分离出Redis关于秒杀问题的核心知识点进行讲解!

    • 全局唯一ID
    • 实现优惠券秒杀下单  
    • 超卖问题  
    • 一人一单
    • 分布式锁  
    • Redis优化秒杀  
    • Redis消息队列实现异步秒杀

    全局唯一ID 

    场景分析 

    首先,我们依照黑马的项目来进行分析,在什么情况下要使用到这个全局唯一ID。

    在黑马点评这个项目中,使用的商品其实也就是优惠券

    当用户抢购时,就会生成订单并保存到 tb_voucher_order 这张表中 

    1. CREATE TABLE `tb_voucher_order` (
    2. `id` bigint NOT NULL COMMENT '主键',
    3. `user_id` bigint UNSIGNED NOT NULL COMMENT '下单的用户id',
    4. `voucher_id` bigint UNSIGNED NOT NULL COMMENT '购买的代金券id',
    5. `pay_type` tinyint UNSIGNED NOT NULL DEFAULT 1 COMMENT '支付方式 1:余额支付;2:支付宝;3:微信',
    6. `status` tinyint UNSIGNED NOT NULL DEFAULT 1 COMMENT '订单状态,1:未支付;2:已支付;3:已核销;4:已取消;5:退款中;6:已退款',
    7. `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '下单时间',
    8. `pay_time` timestamp NULL DEFAULT NULL COMMENT '支付时间',
    9. `use_time` timestamp NULL DEFAULT NULL COMMENT '核销时间',
    10. `refund_time` timestamp NULL DEFAULT NULL COMMENT '退款时间',
    11. `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    12. PRIMARY KEY (`id`) USING BTREE
    13. ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = COMPACT;

    但是,这张SQL表里面的主键id,是不可以使用自增的!!! 

    不能用自增的原因

    id的规律性太明显

    如果使用自增的话,用户可以根据两笔订单的ID,来判断这段时间内订单的量。 

    受单表数据量的限制

    订单的数据量一般很大,一天可能会有几百万,如果使用自增ID,就很难分库分表了!

    全局唯一ID的条件

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

    全局唯一ID的Redis实现

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

    符号位1bit,永远为0 

    时间戳31bit,以秒为单位,可以使用69

    序列号32bit,秒内的计数器,支持每秒产生2^32个不同ID 

    代码实现

    RedisIdWorker-Redis全局ID生成器工具类

    1. /**
    2. * Redis的全局ID生成器
    3. */
    4. @Component
    5. public class RedisIdWorker {
    6. /**
    7. * 开始时间戳
    8. * 2022.1.1的时间戳
    9. */
    10. private static final long BEGIN_TIMESTAMP = 1640995200L;
    11. /**
    12. * 序列号的位数
    13. */
    14. private static final int COUNT_BITS = 32;
    15. private StringRedisTemplate stringRedisTemplate;
    16. // 构造器注入
    17. public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
    18. this.stringRedisTemplate = stringRedisTemplate;
    19. }
    20. /**
    21. * 生成全局ID
    22. * Long 类型 8个字节 64个bit
    23. * 符号位(1bit) + 时间戳(31bit) + 序列号(32bit)
    24. * @param keyPrefix
    25. * @return
    26. */
    27. public long nextId(String keyPrefix) {
    28. // 1.生成时间戳
    29. LocalDateTime now = LocalDateTime.now();
    30. long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
    31. // ID的时间戳
    32. long timestamp = nowSecond - BEGIN_TIMESTAMP;
    33. // 2.生成序列号
    34. // 2.1.获取当前日期,精确到天
    35. String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
    36. // 2.2.自增长
    37. // icr表示自增长,keyPrefix表示业务类型,一天一个key
    38. // 例如: icr:order:2022:11:16
    39. long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
    40. // 3.拼接并返回 (位运算)
    41. return timestamp << COUNT_BITS | count;
    42. }
    43. }

    位运算实现字符串拼接 

    timestamp << COUNT_BITS | count 

    将 timestamp 向左移动32位,在与 count 做“或”运算(一个为真就为真!)

    单元测试 

    我们可以看看并发情况下,该工具类的性能怎么样

    1. @Resource
    2. private RedisIdWorker redisIdWorker;
    3. private ExecutorService es = Executors.newFixedThreadPool(500);
    4. @Test
    5. void testIdWorker() throws InterruptedException {
    6. CountDownLatch latch = new CountDownLatch(300);
    7. Runnable task = () -> {
    8. for (int i = 0; i < 100; i++) {
    9. long id = redisIdWorker.nextId("order");
    10. System.out.println("id = " + id);
    11. }
    12. latch.countDown();
    13. };
    14. long start = System.currentTimeMillis();
    15. for (int i = 0; i < 300; i++) {
    16. es.submit(task);
    17. }
    18. latch.await();
    19. long end = System.currentTimeMillis();
    20. System.out.println(end - start);
    21. }

    运行结果如下

    生成3万个订单ID 

    其它全局唯一ID的生成策略 

    • UUID
    • Redis自增
    • snowflake算法
    • 数据库自增

    秒杀下单 

    场景分析 

    每个店铺都可以发布优惠券,分为平价券和特价券。平价券可以任意购买,而特价券需要秒杀抢购

    tb_voucher:优惠券的基本信息,优惠金额、使用规则等(普通券+秒杀券)

    1. CREATE TABLE `tb_voucher` (
    2. `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
    3. `shop_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '商铺id',
    4. `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '代金券标题',
    5. `sub_title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '副标题',
    6. `rules` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '使用规则',
    7. `pay_value` bigint UNSIGNED NOT NULL COMMENT '支付金额,单位是分。例如200代表2元',
    8. `actual_value` bigint NOT NULL COMMENT '抵扣金额,单位是分。例如200代表2元',
    9. `type` tinyint UNSIGNED NOT NULL DEFAULT 0 COMMENT '0,普通券;1,秒杀券',
    10. `status` tinyint UNSIGNED NOT NULL DEFAULT 1 COMMENT '1,上架; 2,下架; 3,过期',
    11. `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    12. `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    13. PRIMARY KEY (`id`) USING BTREE
    14. ) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = COMPACT;

    tb_seckill_voucher:优惠券的库存、开始抢购时间,结束抢购时间。特价优惠券才需要填写这些信息(秒杀券拓展字段)

    1. CREATE TABLE `tb_seckill_voucher` (
    2. `voucher_id` bigint UNSIGNED NOT NULL COMMENT '关联的优惠券的id',
    3. `stock` int NOT NULL COMMENT '库存',
    4. `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    5. `begin_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '生效时间',
    6. `end_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '失效时间',
    7. `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    8. PRIMARY KEY (`voucher_id`) USING BTREE
    9. ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '秒杀优惠券表,与优惠券是一对一关系' ROW_FORMAT = COMPACT;

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

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

    代码实现 

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

    存在问题

    上述代码是写好了,运行起来看起来页没有什么问题,但是在多线程,高并发的场景下就会出现大问题,100%会发生超卖的情况!!!

  • 相关阅读:
    C#里氏替换
    Python基础知识整理 01-变量、数据类型、运算符、判断语句、循环语句
    8、Feign远程调用
    Newtonsoft.Json/Json.NET忽略序列化时的意外错误
    C++ Primer 总结索引 | 第八章:IO库
    好物推荐:文字转语音朗读软件哪个好?
    【深度学习】目标检测的性能评价指标,mAP_0.5,mAP_0.5,0.95,0.05
    VS2019编译一个带qrc项目时出现的问题
    深度学习笔记之优化算法(三)动量法的简单认识
    uart1接收不定长度数据和发送:STM32 HAL库串口+DMA+IDLE空闲中断
  • 原文地址:https://blog.csdn.net/weixin_43715214/article/details/127876980