• 黑马点评--优惠卷秒杀


    黑马点评–优惠卷秒杀

    全局ID生成器:

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

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lvc2qD5J-1668574944843)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221113221848596.png)]

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

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FM7N8l3N-1668574944844)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221113222812554.png)]

    Redis自增ID策略:

    • 每天一个key,方便统计订单量
    • iD结构是时间戳+计数器
        /**
         * 开始时间戳
         */
        private static final long BEGIN_TIMESTAMP = 1640995200;
        /**
         * 序列号的位数
         */
        private static final int COUNT_BITS = 32;
        /**
         * id生成策略
         *
         * @param keyPrefix 业务前缀
         * @return
         */
        @Resource
        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.拼接并返回
    
            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);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    测试ID自增生成策略:

    500个线程每一个线程生成100id一共50000个id花的时间:

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

    每个店铺都可以发布优惠卷:

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

    • id的规律太明显
    • 受单表数据量的限制

    全局唯一ID生成策略:

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

    实现优惠券秒杀下单

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

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lLOQ08x3-1668574944845)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221114160638726.png)]

    表关系如下:

    • tb_voucher:优惠劵的基本信息,优惠金额,使用规则等
    • tb_seckill_voucher:优惠劵的库存,开始抢购时间,结束抢购时间。特价优惠券才需要填写这些信息

    在VoucherController中提供一个接口,可以添加秒杀优惠券:http://localhost:8081/voucher/seckill

    {
    "shopId":1,
    "title":"100元代金券",
    "subTitle":"周一至周五均可使用",
    "rules": "全场通用\\n无需预约\\n可无限叠加\\不兑换、不找零\\n仅限堂食",
    "payValue": 8000,
    "actualValue": 10000,
    "type": 1,
    "stock": 100,
    "beginTime": "2022-10-25T12:09:04",
    "endTime": "2022-12-25T12:09:04"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    可以通过postman调用

    实现秒杀下单:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v54T0KOV-1668574944846)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221114192754339.png)]

    下单时需要判断两点:

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

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1AtDzqAS-1668574944846)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221114193239439.png)]

    库存超卖问题分析:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oMPRwTiA-1668574944847)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221114233014147.png)]

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

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iKY8HQuU-1668574944848)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221114233544182.png)]

    乐观锁:

    • 版本号法
      - [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JBV8SEkv-1668574944849)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221114234218883.png)]

    • CAS法(CompareAndSet)

      • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4q4SdHyN-1668574944849)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221114234628336.png)]

    乐观锁解决超卖问题:

    //1.查询优惠劵
    SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
    //2.判断秒杀是否开始
    LocalDateTime beginTime = voucher.getBeginTime();
    if (beginTime.isAfter(LocalDateTime.now())) {
        //尚未开始
        return Result.fail("活动尚未开始");
    }
    //3.判断秒杀是否已经结束
    LocalDateTime endTime = voucher.getEndTime();
    if (LocalDateTime.now().isAfter(endTime)) {
        //已结束
        return Result.fail("活动已经结束");
    }
    //4判断库存是否充足
    if (voucher.getStock() < 1) {
        //库存不足
        return Result.fail("库存不足!");
    }
    //5.扣减库存
    boolean success =iSeckillVoucherService.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 订单id
    long orderId = redisIdWorker.nextId("order");
    voucherOrder.setId(orderId);
    //6.2 用户id
    Long userId = UserHolder.getUser().getId();
    voucherOrder.setUserId(userId);
    //6.3 代金券id
    voucherOrder.setVoucherId(voucherId);
    save(voucherOrder);
    // 7.返回订单id
    return Result.ok(orderId);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    超卖这样的线程安全问题,解决方案有哪些?

    1.悲观锁:添加同步锁,让线程串行执行

    • 优点:简单粗暴
    • 缺点:性能一般

    2.乐观锁:不加锁,在更新时判读是否有其它线程在修改

    一人一单:

    需求:修改秒杀业务,要求同一个优惠券,一个用户只能下一单

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CDASAcEo-1668575438357)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221116130925403.png)]

        @Autowired
        private ISeckillVoucherService iSeckillVoucherService;
    
        @Autowired
        private RedisIdWorker redisIdWorker;
    
        @Override
        public Result seckillVoucher(Long voucherId) {
            //1.查询优惠劵
            SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
            //2.判断秒杀是否开始
            LocalDateTime beginTime = voucher.getBeginTime();
            if (beginTime.isAfter(LocalDateTime.now())) {
                //尚未开始
                return Result.fail("活动尚未开始");
            }
            //3.判断秒杀是否已经结束
            LocalDateTime endTime = voucher.getEndTime();
            if (LocalDateTime.now().isAfter(endTime)) {
                //已结束
                return Result.fail("活动已经结束");
            }
            //4判断库存是否充足
            if (voucher.getStock() < 1) {
                //库存不足
                return Result.fail("库存不足!");
            }
            Long userId = UserHolder.getUser().getId();
            synchronized (userId.toString().intern()){
                //获取spring事务代理对象
                IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
                return proxy.createVoucherOrder(voucherId);
        }
    }
    
        @Transactional
        public  Result createVoucherOrder(Long voucherId) {
                //6.一个人一单
                Long userId = UserHolder.getUser().getId();
                //6.1查询订单
                int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
                //6.2判断是否存在
                if (count > 0) {
                    //用户以及购买过
                    return Result.fail("用户已经购买过一次");
                }
                //7.扣减库存
                boolean success = iSeckillVoucherService.update()
                        .setSql("stock =stock -1")
                        .eq("voucher_id", voucherId)
                        .gt("stock", 0).update();
                if (!success) {
                    //扣减失败
                    return Result.fail("库存不足!");
                }
                //8.创建订单
                VoucherOrder voucherOrder = new VoucherOrder();
                //8.1 订单id
                long orderId = redisIdWorker.nextId("order");
                voucherOrder.setId(orderId);
                //8.2 用户id
                voucherOrder.setUserId(userId);
                //8.3 代金券id
                voucherOrder.setVoucherId(voucherId);
                save(voucherOrder);
                // 9.返回订单id
                return Result.ok(orderId);
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68

    一人一单的并发安全问题:

    通过加锁可以解决在单机情况下的一人一单安全问题,但是在集群模式下就不行了。

    1.我们将服务启动两份,端口分别为8081和8082:

    在这里插入图片描述

    2.修改nginx的conf目录下的nginx.conf文件,配置反向代理和负载均衡:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bF5Dvek3-1668574944852)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221116123310064.png)]

    现在,用户请求会在这两个节点上负载均衡,再次测试下是否存在线程安全问题。

    一人一单的并发安全问题:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6Ikza5ar-1668574944856)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221116125039581.png)]

  • 相关阅读:
    【云原生】简单谈谈海量数据采集组件Flume的理解
    [Codeforces] number theory (R1200) Part.8
    Mybatis表的关联查询
    Bigemap 在生态环境督察工作中的应用
    黑盒测试-场景法
    第2-3-6章 打包批量下载附件的接口开发-文件存储服务系统-nginx/fastDFS/minio/阿里云oss/七牛云oss
    洛谷刷题C语言:询问、光图、苏联人、Another Cow Number Game G、STROJOPIS
    CSS高级的详细解析
    java基于ssm的电子资源管理系统 毕业设计
    【图论算法】深度优先搜索的应用
  • 原文地址:https://blog.csdn.net/weixin_53050118/article/details/127883193