ToB企服应用市场:ToB评测及商务社交产业平台

标题: day08-优惠券秒杀04 [打印本页]

作者: 河曲智叟    时间: 2023-4-28 21:23
标题: day08-优惠券秒杀04
功能03-优惠券秒杀04

4.功能03-优惠券秒杀

4.7Redis优化秒杀

4.7.1优化分析

现在来回顾一下优惠券秒杀业务的两个主要问题:
(1)首先是对优惠券的扣减,需要防止库存超卖现象;
(2)其次,需要对每个用户下单数量进行限制,实现一人一单的功能。
处理秒杀优惠券的业务:
因为整个过程有很多对数据库的操作(查询优惠券、查询订单、减库存、创建订单),因此这个业务的性能并不是很好:
优化前:
上述业务看似复杂,实际上只有两个过程:(1)对于用户资格的校验:库存够不够,该用户买没买过(一人一单)(2)然后才是真正的下单业务。
我们可以对这两个过程进行分离,别分使用两个线程进行操作:主线程负责对用户购买资格的校验,如果有购买的资格,再开启一个独立的线程,来处理耗时较久的减库存和创建订单操作。
为了提高效率,使用redis判断秒杀库存和校验一人一单,如果校验通过,则redis会记录优惠券信息、用户信息、订单信息到阻塞队列。一方面:tomcat服务器去读取这个队列的信息,完成下单。另一方面:redis给用户返回一个订单号,代表该用户抢单成功,用户可以根据这个订单号去付款。
优化后:
这样,整个秒杀流程就变为:直接在redis中判断用户的秒杀资格和库存,然后将信息保存到队列里。
秒杀业务的流程变短了,而且是基于Redis,性能得到很大的提升,整个业务的吞吐能力、并发能力可以大大提高了。
那么,如何在Redis中完成对秒杀库存的判断和一人一单的判断呢?
首先是对数据的存储:
优化后,在Redis中需要执行的具体流程:
异步秒杀优化总结:
上述的优化操作,一方面缩短了秒杀业务的流程,从而大大提高了秒杀业务的并发;另一方面,redis的操作和数据库的操作是异步的,对数据库操作的时效性不再要求那么高了,减轻了数据库的压力。
4.7.2代码实现

改进秒杀业务,提高并发性能。需求:
需求1:新增秒杀优惠券的同时,将优惠券信息保存到Redis中
(1.1)修改IVoucherService
  1. package com.hmdp.service;
  2. import com.baomidou.mybatisplus.extension.service.IService;
  3. import com.hmdp.dto.Result;
  4. import com.hmdp.entity.Voucher;
  5. /**
  6. *  服务类
  7. *
  8. * @author 李
  9. * @version 1.0
  10. */
  11. public interface IVoucherService extends IService<Voucher> {
  12.     void addSeckillVoucher(Voucher voucher);
  13. }
复制代码
(1.2)修改VoucherServiceImpl
  1. package com.hmdp.service.impl;
  2. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
  3. import com.hmdp.entity.Voucher;
  4. import com.hmdp.mapper.VoucherMapper;
  5. import com.hmdp.entity.SeckillVoucher;
  6. import com.hmdp.service.ISeckillVoucherService;
  7. import com.hmdp.service.IVoucherService;
  8. import org.springframework.data.redis.core.StringRedisTemplate;
  9. import org.springframework.stereotype.Service;
  10. import org.springframework.transaction.annotation.Transactional;
  11. import javax.annotation.Resource;
  12. import static com.hmdp.utils.RedisConstants.SECKILL_STOCK_KEY;
  13. /**
  14. * 服务实现类
  15. *
  16. * @author 李
  17. * @version 1.0
  18. */
  19. @Service
  20. public class VoucherServiceImpl extends ServiceImpl<VoucherMapper, Voucher> implements IVoucherService {
  21.     @Resource
  22.     private ISeckillVoucherService seckillVoucherService;
  23.     @Resource
  24.     private StringRedisTemplate stringRedisTemplate;
  25.     @Override
  26.     @Transactional
  27.     public void addSeckillVoucher(Voucher voucher) {
  28.         // 保存优惠券到数据库
  29.         save(voucher);
  30.         // 保存秒杀优惠券信息到数据库
  31.         SeckillVoucher seckillVoucher = new SeckillVoucher();
  32.         seckillVoucher.setVoucherId(voucher.getId());
  33.         seckillVoucher.setStock(voucher.getStock());
  34.         seckillVoucher.setBeginTime(voucher.getBeginTime());
  35.         seckillVoucher.setEndTime(voucher.getEndTime());
  36.         seckillVoucherService.save(seckillVoucher);
  37.         //保存秒杀库存到Redis中
  38.         stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
  39.     }
  40. }
复制代码
(1.3)修改VoucherController
  1. package com.hmdp.controller;
  2. import com.hmdp.dto.Result;
  3. import com.hmdp.entity.Voucher;
  4. import com.hmdp.service.IVoucherService;
  5. import org.springframework.web.bind.annotation.*;
  6. import javax.annotation.Resource;
  7. /**
  8. * 前端控制器
  9. *
  10. * @author 李
  11. * @version 1.0
  12. */
  13. @RestController
  14. @RequestMapping("/voucher")
  15. public class VoucherController {
  16.     @Resource
  17.     private IVoucherService voucherService;
  18.    
  19.     /**
  20.      * 新增秒杀券
  21.      * @param voucher 优惠券信息,包含秒杀信息
  22.      * @return 优惠券id
  23.      */
  24.     @PostMapping("seckill")
  25.     public Result addSeckillVoucher(@RequestBody Voucher voucher) {
  26.         voucherService.addSeckillVoucher(voucher);
  27.         return Result.ok(voucher.getId());
  28.     }
  29. }
复制代码
(1.4)使用postman进行测试,返回结过显示插入成功,data为插入的秒杀券的id
数据库和Redis中也分别插入成功了:
需求2:基于Lua脚本,判断秒杀库存、一人一单,决定用户是否抢占成功
在resources目录下新建一个Lua脚本
seckill.lua:
  1. -- 1.参数列表
  2. --    1.1 优惠券id
  3. local voucherId = ARGV[1]
  4. --    1.2 用户id
  5. local userId = ARGV[2]
  6. -- 2.数据key
  7. --  2.1 库存key
  8. local stockKey = 'seckill:stock:' .. voucherId
  9. --  2.2 订单key
  10. local orderKey = 'seckill:order' .. voucherId
  11. -- 3.脚本业务
  12. --  3.1判断库存是否充足 get stockKey
  13. if (tonumber(redis.call('get', stockKey)) <= 0) then
  14.     -- 3.2库存不足,返回1
  15.     return 1
  16. end
  17. -- 3.3库存充足,判断用户是否下过单(判断用户id是否在订单key对应的集合中)
  18. -- sismember orderKey userId
  19. if (redis.call('sismember', orderKey, userId) == 1) then
  20.     -- 3.4 若存在,说明是重复下单,返回2
  21.     return 2
  22. end
  23. -- 3.5 扣库存 incrby stockKey -1
  24. redis.call('incrby', stockKey, -1)
  25. -- 3.6 下单(保存用户) sadd orderKey userId
  26. redis.call('sadd', orderKey, userId)
  27. return 0
复制代码
重启项目,进行测试:
(1)初始数据:
(2)使用jemeter进行测试:使用1000个不同的用户同时向服务器发送抢购秒杀券的请求
测试结果:可以看到平均响应实现为216毫秒,最小值为17毫秒,比之前平均500毫秒的响应时间缩短了一半。
4.7.3秒杀优化总结

(1)秒杀业务的优化思路是什么?
(2)基于阻塞队列的异步秒杀存在哪些问题?
我们将在接下来的分析中对上述两个问题进行解决。
4.8Redis消息队列实现异步秒杀

要解决上面的两个问题,最佳的解决方案就是使用消息队列
4.8.1什么是消息队列

消息队列(Message Queue,简称MQ),字面意思就是存放消息的队列。最简单的消息队列模型包括3个角色:
4.8.2消息队列实现异步秒杀的优势

使用消息队列实现异步秒杀的优势:
当下比较知名的消息引擎,包括:ActiveMQ、RabbitMQ、Kafka、RocketMQ、Artemis 等
这里使用Redis实现消息队列:
Redis提供了三种不同的方式来实现消息队列:
4.8.3基于List结构模拟的消息队列

Redis的List数据结构是一个双向链表,很容易模拟出队列效果。队列是入口和出口不在一边,我们可以利用:LPUSH结合RPOP、或者RPUSH结合LPOP来实现。
不过要注意的是,当队列中没有消息时,RPOP或LPOP操作会返回null,并不像JVM的阻塞队列那样会阻塞并等待消息。因此这里应该使用BRPOP或者BLPOP来实现阻塞效果。
  1. package com.hmdp.service.impl;
  2. import com.hmdp.dto.Result;
  3. import com.hmdp.entity.VoucherOrder;
  4. import com.hmdp.mapper.VoucherOrderMapper;
  5. import com.hmdp.service.ISeckillVoucherService;
  6. import com.hmdp.service.IVoucherOrderService;
  7. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
  8. import com.hmdp.utils.RedisIdWorker;
  9. import com.hmdp.utils.UserHolder;
  10. import lombok.extern.slf4j.Slf4j;
  11. import org.redisson.api.RLock;
  12. import org.redisson.api.RedissonClient;
  13. import org.springframework.aop.framework.AopContext;
  14. import org.springframework.core.io.ClassPathResource;
  15. import org.springframework.data.redis.core.StringRedisTemplate;
  16. import org.springframework.data.redis.core.script.DefaultRedisScript;
  17. import org.springframework.stereotype.Service;
  18. import org.springframework.transaction.annotation.Transactional;
  19. import javax.annotation.PostConstruct;
  20. import javax.annotation.Resource;
  21. import java.util.Collections;
  22. import java.util.concurrent.*;
  23. /**
  24. * 服务实现类
  25. *
  26. * @author 李
  27. * @version 1.0
  28. */
  29. @Service
  30. @Slf4j
  31. public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
  32.     @Resource
  33.     private ISeckillVoucherService seckillVoucherService;
  34.     @Resource
  35.     private RedisIdWorker redisIdWorker;
  36.     @Resource
  37.     private StringRedisTemplate stringRedisTemplate;
  38.     @Resource
  39.     private RedissonClient redissonClient;
  40.     private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
  41.     //类一加载就初始化脚本
  42.     static {
  43.         SECKILL_SCRIPT = new DefaultRedisScript<>();
  44.         SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
  45.         SECKILL_SCRIPT.setResultType(Long.class);
  46.     }
  47.     //阻塞队列:当一个线程尝试从队列中获取元素时,如果队列中没有元素,那么该线程就会被阻塞,直到队列中有元素,线程才会被唤醒并获取元素
  48.     private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024);
  49.     //线程池
  50.     private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
  51.     //在当前类初始化完毕之后就执行
  52.     @PostConstruct
  53.     private void init() {
  54.         SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
  55.     }
  56.     //执行异步操作,从阻塞队列中获取订单
  57.     private class VoucherOrderHandler implements Runnable {
  58.         @Override
  59.         public void run() {
  60.             while (true) {
  61.                 try {
  62.                     //1.获取队列中的订单信息
  63.                     /* take()--获取和删除阻塞对列中的头部,如果需要则等待直到元素可用
  64.                                (因此不必担心这里的死循环会增加cpu的负担) */
  65.                     VoucherOrder voucherOrder = orderTasks.take();
  66.                     //2.创建订单
  67.                     handleVoucherOrder(voucherOrder);
  68.                 } catch (Exception e) {
  69.                     log.error("处理订单异常", e);
  70.                 }
  71.             }
  72.         }
  73.     }
  74.     private IVoucherOrderService proxy;
  75.     private void handleVoucherOrder(VoucherOrder voucherOrder) {
  76.         //获取用户(因为目前的是线程池对象,不是主线程,不能使用UserHolder从ThreadLocal中获取用户id)
  77.         Long userId = voucherOrder.getUserId();
  78.         //创建锁对象,指定锁的名称
  79.         RLock lock = redissonClient.getLock("lock:order:" + userId);
  80.         //获取锁(可重入锁)
  81.         boolean isLock = lock.tryLock();
  82.         //判断是否获取锁成功
  83.         if (!isLock) {
  84.             //获取锁失败
  85.             log.error("不允许重复下单");
  86.         }
  87.         try {
  88.             proxy.createVoucherOrder(voucherOrder);
  89.         } finally {
  90.             //释放锁
  91.             lock.unlock();
  92.         }
  93.     }
  94.     @Override
  95.     public Result seckillVoucher(Long voucherId) {
  96.         //获取用户id
  97.         Long userId = UserHolder.getUser().getId();
  98.         //1.执行lua脚本
  99.         Long result = stringRedisTemplate.execute(
  100.                 SECKILL_SCRIPT,
  101.                 Collections.emptyList(),
  102.                 voucherId.toString(),
  103.                 userId.toString()
  104.         );
  105.         //2.判断脚本执行结果是否为0
  106.         int r = result.intValue();
  107.         if (r != 0) {
  108.             //2.1如果不为0,代表没有购买资格
  109.             return Result.fail(r == 1 ? "库存不足" : "不能重复下单");
  110.         }
  111.         //2.2如果为0,代表有购买资格,将下单信息保存到阻塞对列中
  112.         VoucherOrder voucherOrder = new VoucherOrder();
  113.         //设置订单id
  114.         long orderId = redisIdWorker.nextId("order");
  115.         voucherOrder.setId(orderId);
  116.         //设置用户id
  117.         voucherOrder.setUserId(userId);
  118.         //设置秒杀券id
  119.         voucherOrder.setVoucherId(voucherId);
  120.         //将上述信息保存到阻塞队列
  121.         orderTasks.add(voucherOrder);
  122.         //3.获取代理对象
  123.         proxy = (IVoucherOrderService) AopContext.currentProxy();
  124.         //4.返回订单id
  125.         return Result.ok(0);
  126.     }
  127.     @Transactional
  128.     public void createVoucherOrder(VoucherOrder voucherOrder) {
  129.         //一人一单
  130.         Long userId = voucherOrder.getUserId();
  131.         //查询订单
  132.         int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();
  133.         if (count > 0) {//说明已经该用户已经对该优惠券下过单了
  134.             log.error("用户已经购买过一次!");
  135.             return;
  136.         }
  137.         //库存充足,则扣减库存(操作秒杀券表)
  138.         boolean success = seckillVoucherService.update()
  139.                 .setSql("stock = stock -1")//set stock = stock -1
  140.                 //where voucher_id =? and stock>0
  141.                 .gt("stock", 0).eq("voucher_id", voucherOrder.getVoucherId()).update();
  142.         if (!success) {//操作失败
  143.             log.error("秒杀券库存不足!");
  144.             return;
  145.         }
  146.         //将订单写入数据库(操作优惠券订单表)
  147.         save(voucherOrder);
  148.     }
  149. }
复制代码
基于List的消息队列有哪些优缺点?
优点:
缺点:
4.8.4基于PubSub的消息队列

PubSub(发布订阅)是Redis2.0版本引入的消息传递模型。顾名思义,消费者可以订阅一个或者多个channel,生产者向对应channel发送消息后,所有订阅者都能收到相关消息。
基于PubSub的消息队列有哪些优缺点?
优点:采用发布订阅模型,支持多生产、多消费
缺点:
4.8.5基于Stream的消息队列

Stream是Redis5.0引入的一种新的数据类型,可以实现一个功能非常完善的消息队列。
发送消息的命令:

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4