【Redis学习07】Redis优化秒杀--使用阻塞队列实现异步秒杀

1. 秒杀流程分析

优化秒杀流程之前,我们先来看一下之前秒杀的实现流程
在这里插入图片描述
我们分析一下:首先用户发送下单请求,通过Nginx负载均衡将请求发送到我们的tomcat服务器,服务器响应请求后开始查询优惠券等一系列操作,最后将结果返回给用户

试想一下,我们的程序是不是同时完成这些操作。也就是说,一次只能响应一个用户的请求,当很多用户同时发送请求时,用户只能等前面一个用户完成相对应的下单流程才能进行下单操作,这样在高并发模式下我们程序的性能是不是会很差

怎么解决呢?

我们是不是能这样设想一下,只要用户拥有下单资格,我们就能通知用户下单成功,然后我们重新启动 一个线程来完成用户下单的操作。每个用户只要有下单资格我们就通知用户下单成功,这样程序的响应速度就会加快,我们程序的性能就上去了吗。

2. Redis优化秒杀

OK,经过上面的分析,我们就能将想法转变为如下的流程图
在这里插入图片描述
首先我们判断库存是否充足,再判断用户是否已经下过订单,如果两者都满足就提示客户下单成功,这时我们将优惠券id,用户id,订单id添加到阻塞队列,重新启动一个新的线程来处理相关的业务逻辑

3. 优化秒杀代码实现

3.1 流程分析

为了保证用户下单资格的原子性,我们使用lua脚本判断用户是否拥有下单资格。有的话返回0,否则返回其他标识。

接下来我们执行lua脚本,判断结果是否为0,不为零就返回异常信息,否则就将优惠券id,用户id,订单id添加到阻塞队列,执行异步下单,最后返回订单id

这里我们明确一下,一是将优惠券库存信息放入redis,使用的是String类型。二是保存优惠券id,用户id,订单id,这个我们需要使用的是Set集合类型。
在这里插入图片描述

3.2 需求分析

在这里插入图片描述

3.3 代码实现

  1. 将优惠券库存信息保存到redis中
@Override
    @Transactional
    public void addSeckillVoucher(Voucher voucher) {
        // 保存优惠券
        save(voucher);
        // 保存秒杀信息
        SeckillVoucher seckillVoucher = new SeckillVoucher();
        seckillVoucher.setVoucherId(voucher.getId());
        seckillVoucher.setStock(voucher.getStock());
        seckillVoucher.setBeginTime(voucher.getBeginTime());
        seckillVoucher.setEndTime(voucher.getEndTime());
        seckillVoucherService.save(seckillVoucher);
        stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY+voucher.getId(),voucher.getStock().toString());
    }
  1. lua脚本代码:判断库存是否充足并且用户是否已经下过订单
-- 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. 判断用户是否具有下单资格,创建订单

    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 BlockingQueue<VoucherOrder> orderTask = new ArrayBlockingQueue<>(1024*1024);
    /**
     * 优惠券秒杀
     *
     * @param voucherId
     * @return
     */
    @Override
    @Transactional
    public Result seckillVoucher(Long voucherId) {

        Long userId = UserHolder.getUser().getId();

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

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

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

        //2. 创建订单
        //2.1 设置id
        VoucherOrder voucherOrder = new VoucherOrder();
        long voucherOrderId = RedisIdWorker.nextId("voucherOrder");
        voucherOrder.setId(voucherOrderId);

        //2.2 设置user_id
        voucherOrder.setUserId(userId);

        //2.3 设置优惠券id
        voucherOrder.setVoucherId(voucherId);

        //2.4放入阻塞队列
        orderTask.add(voucherOrder);

        return Result.ok(voucherOrderId);
    }
}

4. 异步执行下单操作

	//定义阻塞队列,当队列为空时阻塞,不为空才执行相关操作
    private BlockingQueue<VoucherOrder> orderTask = new ArrayBlockingQueue<>(1024*1024);

    //定义一个线程池,异步执行下单操作
    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() {
            while (true){
                try {
                    //获取队列中的订单信息
                    VoucherOrder voucherOrder = orderTask.take();
                    //创建订单
                    createVoucherOrder(voucherOrder);

                } 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();
        }
    }

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

4. Redis优化秒杀总结以及存在问题

在这里插入图片描述
我们分析一下使用阻塞队列完成异步秒杀存在的问题

  • 内存限制问题:我们自定义的阻塞队列的大小是我们自己设置的,一旦订单数量过多,导致阻塞队列内存占满,此时就会有订单丢失的风险。

  • 数据安全问题:一旦我们redis服务宕机,阻塞队列的内存就会被清空,用户下单的数据也会随之丢失,因此存在数据的安全问题。

如何解决使用阻塞队列引发的内存限制和数据安全问题呢?

我会在下一篇博客中跟小伙伴们继续分享。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值