【Redis学习08】Redis消息队列实现异步秒杀

本文通过介绍基于Redis Stream的消息队列,重点讲解了如何利用Stream和消费者组解决基于List、PubSub和Stream的弊端,实现异步秒杀功能。在消费者组中,详细阐述了消费者监听消息的基本思路,并给出了Lua脚本实现用户下单资格检查,最后展示了使用Spring Boot和Redis实现异步秒杀的完整代码,确保了下单过程的高效和稳定性。
摘要由CSDN通过智能技术生成

1. 消息队列

在这里插入图片描述
在这里插入图片描述

1.1 基于List结构模拟消息队列

在这里插入图片描述
在这里插入图片描述

1.2 基于PubSub的消息队列

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

1.3 基于Stream的消息队列

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
既然上述三种消息队列都有其不可避免的缺点,那我们有没有办法解决呢?

接下来我们介绍的基于Stream的消息队列—消费者组就能解决上述三种消息队列的弊端。

2. 基于Stream的消息队列—消费者组

2.1 消费者组介绍

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.2 消费者监听消息基本思路

我们分析一下下面的伪代码:

首先我们尝试监听消息队列,如果消息队列没有消息,则continue结束本次循环,重试获取信息。如果一直没有消息,则进入阻塞状态,直到消息队列存入消息。

当消息队列有消息后,就开始处理消息,处理完消息后一定要执行ACK命令确认消息已经执行

如果处理消息出现异常,则将消息存入pendingList(待处理)队列,程序执行过程中会尝试冲待处理队列中取消息,如果待处理队列没有数据,直接退出异常处理的循环,反之就处理异常信息。
在这里插入图片描述

2.3 消费者组总结

在这里插入图片描述

在这里插入图片描述

3. 基于Stream的消息队列–消费者组实现异步秒杀

3.1 需求分析

在这里插入图片描述

3.2 代码实现

3.2.1 创建Stream类型的消息队列

通过redis客户端创建名为stream.orders的消息队列
在这里插入图片描述

3.2.2 编写用户下单资格的lua脚本

用户下单资格的lua脚本我们可以在之前的基础上进行修改,添加订单id,最后将用户id,优惠券id,订单id添加到消息队列

-- 1.参数列表
-- 1.1.优惠券id
local voucherId = ARGV[1]
-- 1.2.用户id
local userId = ARGV[2]
-- 1.3.订单id
local orderId = ARGV[3]

-- 2.数据key
-- 2.1.库存key
local stockKey = 'seckill:stock:' .. voucherId
-- 2.2.订单key
local orderKey = 'seckill:order:' .. voucherId

-- 3.脚本业务
-- 3.1.判断库存是否充足 get stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then
    -- 3.2.库存不足,返回1
    return 1
end
-- 3.2.判断用户是否下单 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then
    -- 3.3.存在,说明是重复下单,返回2
    return 2
end
-- 3.4.扣库存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 3.5.下单(保存用户)sadd orderKey userId
redis.call('sadd', orderKey, userId)
-- 3.6.发送消息到队列中, XADD stream.orders * k1 v1 k2 v2 ...
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
return 0
3.2.3 实现异步秒杀完整代码
@Service
@Slf4j
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Autowired
    private ISeckillVoucherService seckillVoucherService;
    @Autowired
    private RedisIdWorker RedisIdWorker;
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private RedissonClient redissonClient;

    private static final DefaultRedisScript<Long> SECKILL_SCRIPT;

    static {
        SECKILL_SCRIPT = new DefaultRedisScript<>();
        SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
        SECKILL_SCRIPT.setResultType(Long.class);
    }



    //定义一个线程池,异步执行下单操作
    private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();

    //spring提供的PostConstruct注解:类初始化完毕就执行
    @PostConstruct
    public void init(){
        SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderRunnable());
    }

    //定义处理秒杀的线程,该线程应该在类初始化就应该开始执行任务————如何做到?
    //使用spring提供的PostConstruct注解:类初始化完毕就执行
    private class VoucherOrderRunnable implements Runnable{
        @Override
        public void run() {
            String queueName = "stream.orders";
            while (true){
                try {
                    //1.获取消息队列中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAM  >
                    //从消息队列中读消息,g1组,消费者c1,一次读一个,阻塞时间2秒 ,从stream.orders队列读,>未消费的消息
                    List<MapRecord<String, Object, Object>> list = redisTemplate.opsForStream().read(
                            Consumer.from("g1", "c1"),
                            StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
                            StreamOffset.create(queueName, ReadOffset.lastConsumed())
                    );
                    //2.判断消息队列是否为空
                    if(list==null||list.isEmpty()){
                        //如果为空,则说明没有消息,进行下一次循环
                        continue;
                    }
                   //解析消息
                    //因为我们一次读一个,所以索引为0,而我们存入消息队列的是键值对,因此解析出来是map
                    MapRecord<String, Object, Object> record = list.get(0);
                    Map<Object, Object> value = record.getValue();
                    VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);

                    //3.创建订单
                    createVoucherOrder(voucherOrder);
                    //4. 确认消息,XACK stream.orders,g1,id
                    redisTemplate.opsForStream().acknowledge(queueName,"g1",record.getId());

                } catch (Exception e) {
                    log.error("处理订单异常",e);
                    handlePendingList();
                }

            }
        }

        private void handlePendingList() {
            String queueName = "stream.orders";
            while (true){
                try {
                    //1.获取PendingList中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAM  0
                    //从消息队列中读消息,g1组,消费者c1,一次读一个,阻塞时间2秒 ,
                    List<MapRecord<String, Object, Object>> list = redisTemplate.opsForStream().read(
                            Consumer.from("g1", "c1"),
                            StreamReadOptions.empty().count(1),
                            StreamOffset.create(queueName, ReadOffset.from("0"))
                    );
                    //2.判断消息队列是否为空
                    if(list==null||list.isEmpty()){
                        //如果为空,则说明PendingList没有消息,结束循环
                        break;
                    }
                    //解析消息
                    //因为我们一次读一个,所以索引为0,而我们存入消息队列的是键值对,因此解析出来是map
                    MapRecord<String, Object, Object> record = list.get(0);
                    Map<Object, Object> value = record.getValue();
                    VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);

                    //3.创建订单
                    createVoucherOrder(voucherOrder);
                    //4. 确认消息,XACK stream.orders,g1,id
                    redisTemplate.opsForStream().acknowledge(queueName,"g1",record.getId());

                } catch (Exception e) {
                    log.error("处理订单异常",e);
                }

            }
        }
    }

    private void createVoucherOrder(VoucherOrder voucherOrder) {
        Long userId = voucherOrder.getUserId();
        Long voucherId = voucherOrder.getVoucherId();
        RLock lock = redissonClient.getLock("lock:order" + userId);
        //tryLock的三个参数:最大等待时间,锁释放时间,时间单位
        boolean flag = lock.tryLock();//不设置参数默认不等待,释放时间三十秒
        if(!flag){
            return;
        }

        try {
            //一人一单
            int count = this.query().eq("user_id", userId)
                    .eq("voucher_id", voucherId).count();
            if(count>0){
                return ;
            }


            //当更新时查询的库存大于0时进行库存减一
            boolean success = seckillVoucherService.update()
                    .setSql("stock=stock-1")
                    .gt("stock", 0)
                    .eq("voucher_id", voucherId).update();

            if (!success) {
                return;
            }

            //6. 创建订单
            this.save(voucherOrder);

            return ;
        } finally {
            lock.unlock();
        }
    }
    @Override
    @Transactional
    public Result seckillVoucher(Long voucherId) {

        Long userId = UserHolder.getUser().getId();
        long voucherOrderId = RedisIdWorker.nextId("order");

        //使用lua脚本执行原子级别的操作,不会因为线程阻塞导致释放锁发生错误。
        Long result = redisTemplate.execute(SECKILL_SCRIPT,
                Collections.emptyList(),
                voucherId.toString(), userId.toString(),String.valueOf(voucherOrderId));

        //拆箱
        int res = result.intValue();

        //1. 判断库存是否大于0和用户是否已经下单
        if (res != 0) {
            return Result.fail(res == 1 ? "库存不足" : "用户已下单");
        }

        return Result.ok(voucherOrderId);
    }
}

读取消息队列中的消息使用的方法是lastConsumed,也就是从未消费的第一个消息开始消费。
在这里插入图片描述

而读取待处理队列中的消息是从队列第一个开始
在这里插入图片描述

用户下单到提示用户下单成功只需要经过下面的程序,比之前同步执行下单方法的速度快了不少。我们可以使用Jmeter进行响应时间的测试。
在这里插入图片描述
到这里,我们这一部分通过优惠券秒杀介绍了好多内容:

  • 全局唯一ID生成器
  • 实现优惠券秒杀下单
  • 超卖问题如何解决
  • 一人一单如何控制
  • 从解决一人一单的悲观锁到分布式锁
  • 使用阻塞队列实现异步秒杀
  • 使用消息队列实现异步秒杀

这一部分到这里就算结局了,看到这里的小伙伴不妨好好回顾一下每一部分是如何完成的,我们又是如何进行优化的。

念念不忘,必有回响!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值