通过异步使用消息队列优化秒杀

通过异步使用消息队列优化秒杀


同步秒杀流程

    public Result seckillVoucher(Long voucherId) throws InterruptedException {
        SeckillVoucher seckillVoucher = iSeckillVoucherService.getById(voucherId);
        LocalDateTime beginTime = seckillVoucher.getBeginTime();
        if (LocalDateTime.now().isBefore(beginTime)) {
            return Result.fail("秒杀还未开始");
        }
        LocalDateTime endTime = seckillVoucher.getEndTime();
        if (LocalDateTime.now().isAfter(endTime)) {
            return Result.fail("秒杀已经结束");
        }
//        查看库存
        Integer stock = seckillVoucher.getStock();
        if (stock < 1) {
            return Result.fail("库存不足");
        }
        Long userId = UserHolder.getUser().getId();
//        redis集群实现
        RLock lock = redissonClient.getLock("order:" + userId);
        boolean isLock = lock.tryLock(1L, TimeUnit.SECONDS);
        if (!isLock) {
            return Result.fail("你已经购买优惠卷");
        }
        try {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId, userId);
        } finally {
//            redisLock.releaseLock();
            lock.unlock();
        }
    }
  @Transactional
    public Result createVoucherOrder(Long voucherId, Long userId) {
        Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        if (count > 0) {
            return Result.fail("已经购买优惠卷");
        }
//        基于乐观锁避免超卖问题,在乐观锁的基础上做出了改进,当要修改库存时判断库存是否大于0,但是也给数据库带来了巨大的压力
        boolean success = iSeckillVoucherService.update().setSql("stock=stock-1")
                .eq("voucher_id", voucherId).
                gt("stock", 0).update();
        if (!success) {
            return Result.fail("库存不足");
        }
        VoucherOrder voucherOrder = new VoucherOrder();
        long orderId = redisWorker.getGlobalId("order");
        voucherOrder.setId(orderId);
        voucherOrder.setVoucherId(voucherId);
        voucherOrder.setUserId(userId);
        save(voucherOrder);
        return Result.ok(orderId);
    }

以上是我们同步秒杀的流程,首先会从数据库中查询优惠卷,检查库存和是否在秒杀阶段,然后使用redisson基于Redis实现分布式锁解决一人一单问题,如果都通过后就会修改库存并且生成订单,整体的流程如下:

在这个流程中,我们发现整个步骤都是同步,都是交给主线程来执行,负责检验库存保证一人一单然后来修改数据库数据,我们是否可以再请一个人来优化这个流程呢?比如A来负责检验,检验成功后交给B来完成数据库的修改,这样是不是能够优化秒杀的业务

异步优化秒杀

我们可以这样做,让主线程来负责判断这个人能不能抢杀,如果抢杀成功交给另一个人来负责修改数据库,主线程直接返回订单id。

那怎样让主线程快速的库存和一人一单的判断呢?Redis

我们可以将库存保存在Redis的字符串类型中,Redis的性能高于直接查询数据库的性能,而一人一单的校验我们可以使用Redis的set集合,校验是否一人一单就看Redis的set集合中是否含有用户id,而当主线程通过校验后让Redis中的库存-1并且将用户id添加到set集合中。

而我们怎样让修改数据库的任务交给另一个人来处理呢?使用阻塞队列或者消息队列,为了实现简单,暂且使用阻塞队列,我们将要修改数据库的任务放入阻塞队列中,创建一个线程池,让线程池来负责执行修改数据库的任务。

异步秒杀流程

从流程中看出我们主线程只负责进行库存校验和确保一人一单后,生成订单添加到阻塞队列或消息队列中就直接返回,大大提高了业务的性能。

基于lua脚本保证Redis操作原子性

如果我们使用以上的操作来实现代码,会发生并发安全问题,因为校验Redis操作和更新Redis操作不具有原子性,就可能发生问题,例如一个线程通过校验Redis库存等还没有进行修改,另一个线程直接进入比较没有修改的库存,就发生了线程安全问题,我们需要保证这些操作具有原子性

代码实现

lua脚本:

--优惠卷id
local voucherId = ARGV[1];
--用户id
local userId = ARGV[2];
--库存键
local stockKey = "voucher:stock:" + voucherId;
--下过订单的用户键
local orderKey = "voucher:order:" + voucherId;
--查看卷库存是否足够
if (tonumber(redis.call("get", stockKey)) <= 0) then
return 1;
end
if (redis.call("sismember",orderKey,userId)==1) then
return 2
end
--扣库存
redis.call("incrby",stockKey,-1);
--添加用户id
redis.call("sadd",orderKey,userId)
return 0


    /**
     * 异步秒杀
     */
    @Override
    public Result seckillVoucher(Long voucherId) throws InterruptedException {
        Long userId = UserHolder.getUser().getId();
        Long result = redisTemplate.execute(SECkILL_SCRIPT, Collections.emptyList(), voucherId.toString(), userId.toString());
        int re = result.intValue();
        if (re != 0) {
            return re == 1 ? Result.fail("库存不足") : Result.fail("不能重复下单");
        }
        long orderId = redisWorker.getGlobalId("voucher:order");
        VoucherOrder voucherOrder = new VoucherOrder();
        voucherOrder.setVoucherId(voucherId);
        voucherOrder.setId(orderId);
        voucherOrder.setUserId(userId);
        VOUCHER_PROXY = (IVoucherOrderService) AopContext.currentProxy();
        BLOCKING_QUEUE.add(voucherOrder);
        return Result.ok(orderId);
    }

// 初始化lua脚本环境
    private static final DefaultRedisScript<Long> SECkILL_SCRIPT;


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

    //    在类初始化完之后执行
    @PostConstruct
    private void init() {
        EXECUTOR_SERVICE.execute(new VoucherHandler());
    }
// 阻塞队列获取任务处理
    private class VoucherHandler implements Runnable {

        @Override
        public void run() {
            while (true){
                try {
//                从阻塞队列中获取订单信息
                    VoucherOrder voucherOrder = BLOCKING_QUEUE.take();
//                  更新数据库库存以及创建优惠卷订单
                    VOUCHER_PROXY.createVoucherOrder(voucherOrder.getVoucherId(), voucherOrder.getUserId());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    @Transactional
    public Result createVoucherOrder(Long voucherId, Long userId) {
        Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        if (count > 0) {
            return Result.fail("已经购买优惠卷");
        }
//        基于乐观锁避免超卖问题,在乐观锁的基础上做出了改进,当要修改库存时判断库存是否大于0,但是也给数据库带来了巨大的压力
        boolean success = iSeckillVoucherService.update().setSql("stock=stock-1")
                .eq("voucher_id", voucherId).
                gt("stock", 0).update();
        if (!success) {
            return Result.fail("库存不足");
        }
        VoucherOrder voucherOrder = new VoucherOrder();
        long orderId = redisWorker.getGlobalId("order");
        voucherOrder.setId(orderId);
        voucherOrder.setVoucherId(voucherId);
        voucherOrder.setUserId(userId);
        save(voucherOrder);
        return Result.ok(orderId);
    }

阻塞队列的缺点

在我们进行数据库的修改任务时我们使用了阻塞队列来实现,在实际的业务中,我们需要使用一些消息队列来代替阻塞队列,阻塞队列使用的是JVM中的内存,当消息过多时会造成JVM内存爆满,并且功能不够强大,我们可以使用Redis的Stream或者MQ来实现消息队列来代替阻塞队列

相关推荐
taoyong00110 分钟前
Java线程核心01-中断线程的理论原理
java·开发语言
Yhame.36 分钟前
Java 集合框架中的 List、ArrayList 和 泛型 实例
java
coding侠客36 分钟前
Spring Boot 多数据源解决方案:dynamic-datasource-spring-boot-starter 的奥秘
java·spring boot·后端
阿乾之铭40 分钟前
Redis四种模式在Spring Boot框架下的配置
redis
委婉待续42 分钟前
java抽奖系统(八)
java·开发语言·状态模式
weixin_537590451 小时前
《Java编程入门官方教程》第八章练习答案
java·开发语言·servlet
CodeClimb2 小时前
【华为OD-E卷-最左侧冗余覆盖子串 100分(python、java、c++、js、c)】
java·python·华为od
Q_19284999062 小时前
基于Spring Boot的大学就业信息管理系统
java·spring boot·后端
xmh-sxh-13142 小时前
常用数据库类型介绍
java