秒杀优化-Redis+线程池异步秒杀

  秒杀场景下,不同步串行执行业务全流程,将下单、扣库存、创建订单、消息通知等非强即时步骤异步化拆解;前端快速返回结果,后端通过Redis和线程池

   异步秒杀 异步秒杀是高并发秒杀场景下的核心优化方案,区别于传统同步下单模式,它将库存扣减、订单创建、消息推送等耗时业务进行异步拆解处理,仅把权限校验、流量限流、库存预扣等核心逻辑同步执行并快速响应前端请求。借助消息队列、线程池等异步手段削峰填谷,有效隔离瞬时爆发的海量流量,避免大量并发请求直接冲击数据库,大幅降低数据库锁竞争与连接压力,在提升接口响应速度、优化用户体验的同时,保障秒杀系统的高并发承载能力、稳定性与容错性,从根本上解决秒杀活动中系统卡顿、崩溃、超卖等常见问题。

lua脚本

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

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

-- 3.脚本业务
-- 3.1 判断库存是否充足
if(tonumber(redis.call('get',stockKey))<=0) then
    -- 库存不足,返回1
    return 1;
end
--3.2 判断用户是否已购买
if(redis.call('sismember',orderKey,userId) == 1) then
    -- 用户已购买,返回2
    return 2;
end
-- 3.3 减少库存
redis.call('incrby',stockKey,-1)
-- 3.4 记录订单
redis.call('sadd',orderKey,userId)
return 0;

加载lua脚本

//释放秒杀资格判断的lua脚本
    private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
    //static是静态代码块,只执行一次,用于加载lua脚本
    static {
        SECKILL_SCRIPT = new DefaultRedisScript<>();
        SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
        //设置结果类型为Long因为Redis的del指令成功返回1,失败返回0
        SECKILL_SCRIPT.setResultType(Long.class);
    }

主方法

    /**
     * 用lua脚本判断秒杀资格
     * @param voucherId 优惠券id
     * @return 结果
     */
    private IVoucherOrderService proxy;
    @Override
    public Result seckillVoucher(Long voucherId) {
        //1执行lua脚本
        Long userId = UserHolder.getUser().getId();
        //执行脚本
        Long result = stringRedisTemplate.execute(
                SECKILL_SCRIPT,
                //执行 Lua 脚本时,Redis 要求:所有要用到的 Redis key,
                // 必须统一放在 KEYS [] 数组里传入,不能在脚本里拼接。
                //lua脚本没有用集合传key,所以这里传空集合
                Collections.emptyList(),
                voucherId.toString(),
                userId.toString()
        );
        //2.判断结果是否为0
        int r = result.intValue();
        //2.1 不是0,没有购买资格
        if(r != 0){
            return Result.fail(r==1?"库存不足":"不允许重复下单");
        }
        //2.2 是0,有购买资格,把下单信息保存到阻塞队列
        VoucherOrder voucherOrder = new VoucherOrder();
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        voucherOrder.setUserId(userId);
        voucherOrder.setVoucherId(voucherId);
        //放到阻塞队列

        orderTasks.add(voucherOrder);
        //获取代理对象
        //因为你要在 异步线程 里调用 带事务 / 带 AOP 注解 的方法,必须用【当前类的代理对象】去调用,
        // 直接用 this 调用会让事务 / AOP 失效!
        proxy = (IVoucherOrderService) AopContext.currentProxy();
        //3.返回结果,订单id
        return Result.ok(orderId);
    }

阻塞队列

//TODO 阻塞队列--当一个线程尝试从队列中获取元素,没有元素将被阻塞,知道有元素
    private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024*1024);
    //线程池
    private final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();

    @PostConstruct
    /*这是 Spring 提供的注解,标记在方法上,表示该方法会在 Spring 容器初始化完 Bean 之后、
    服务启动时自动执行一次*/
    //开启后台线程
    private void init(){
        SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
    }

    //阻塞队列---实现异步处理订单创建
    private class VoucherOrderHandler implements Runnable{
        @Override
        public void run() {
            // 待实现的业务逻辑(如监听阻塞队列、处理订单)
            while(true){
                try {
                    VoucherOrder voucherOrder = orderTasks.take();
                    //创建订单
                    handleVoucherOrder(voucherOrder);
                } catch (Exception e) {
                    log.error("处理订单异常",e);
                }
            }
        }
    }

再加一层锁

 private void handleVoucherOrder(VoucherOrder voucherOrder) {
        //SimpleRedisLock lock = new SimpleRedisLock("lock:"+userId,stringRedisTemplate);
        //1.获取用户---从线程池中获得
        Long userId = voucherOrder.getUserId();
        //2.创建锁对象
        RLock lock = redissonClient.getLock("lock:order:"+userId);
        //3. 获取锁--互斥效果
        boolean isLock = lock.tryLock();
        if(!isLock){
            log.error("用户{}不允许重复下单",userId);
            return;
        }

        try {

            proxy.createVoucherOrder(voucherOrder);
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
        }
    }

操作数据库

@Transactional
    public void createVoucherOrder(VoucherOrder voucherOrder){
        //一人只能下单一次
        //用户Id
        Long userId = voucherOrder.getUserId();
        int count = query().eq("user_id",userId).eq("voucher_id",voucherOrder.getVoucherId()).count();
        if(count > 0){
            return;
        }
        //5. 减少库存
        boolean success = iSeckillVoucherService.update()
                .setSql("stock = stock - 1")
                .eq("voucher_id", voucherOrder.getVoucherId())//where voucher_id = ?
                //直接判断是否大于0
                //解决库存超卖问题
                .gt("stock", 0)
                .update();
        if(!success){
            return;
        }
        save(voucherOrder);
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值