优惠卷秒杀系统设计秒杀优化 —— 基于阻塞队实现异步秒杀优化 及 基于Lua脚本判断秒杀库存一人一单

Posted Perceus

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了优惠卷秒杀系统设计秒杀优化 —— 基于阻塞队实现异步秒杀优化 及 基于Lua脚本判断秒杀库存一人一单相关的知识,希望对你有一定的参考价值。

(目录)


秒杀优化

1、秒杀优化-异步秒杀思路

回顾一下下单流程:

分成如下几个步骤:


在这六步操作中,又有很多操作是要去操作数据库的,而且还是一个线程串行执行, 这样就会导致我们的程序执行的很慢,所以我们需要异步程序执行,那么如何加速呢?

这种做法好吗?


优化方案:

将耗时比较短的逻辑判断放入到redis中,比如 是否库存足够是否一人一单,这样的操作,只要这种逻辑可以完成,就意味着我们是一定可以下单完成的,我们只需要进行快速的逻辑判断,根本就不用等下单逻辑走完,我们直接给用户返回成功, 再在后台开一个线程,后台线程慢慢的去执行queue里边的消息,这样程序不就超级快了吗?而且也不用担心线程池消耗殆尽的问题,因为这里我们的程序中并没有手动使用任何线程池,当然这里边有两个难点


我们现在来看看整体思路:

当以上判断逻辑走完之后,我们可以判断当前redis中返回的结果是否是0

如果是0,则表示可以下单,则将之前说的信息存入到到queue中去,然后返回,然后再来个线程异步的下单,前端可以通过返回的订单id来判断是否下单成功。


2、秒杀优化-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);
    // 保存秒杀库存到Redis中
    //SECKILL_STOCK_KEY 这个变量定义在RedisConstans中
    //private static final String SECKILL_STOCK_KEY ="seckill:stock:"
    stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());

-- 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

当以上lua表达式执行完毕后,剩下的就是根据步骤3,4来执行我们接下来的任务了

    private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
    // 提前读取脚本 静态代码块
    static 
        SECKILL_SCRIPT = new DefaultRedisScript<>();
        SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua")); // 读取脚本
        SECKILL_SCRIPT.setResultType(Long.class); // 返回值
    


    @Override
    public Result seckillVoucher(Long voucherId) 
        //获取用户
        Long userId = UserHolder.getUser().getId();
        long orderId = redisIdWorker.nextId("order");
        // 1.执行lua脚本
        Long result = stringRedisTemplate.execute(
                SECKILL_SCRIPT,
                Collections.emptyList(),
                voucherId.toString(), userId.toString(), String.valueOf(orderId)
        );
        int r = result.intValue(); // 转成int
        // 2.判断结果是否为0
        if (r != 0) 
            // 2.1.不为0 ,代表没有购买资格
            return Result.fail(r == 1 ? "库存不足" : "不能重复下单");
        
        //TODO 保存阻塞队列
        // 3.返回订单id
        return Result.ok(orderId);
    

3、秒杀优化-基于阻塞队列实现秒杀优化

VoucherOrderServiceImpl 类

完整代码:

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService 

    @Autowired
    private ISeckillVoucherService seckillVoucherService;

    @Autowired
    private RedisIdWorker redisIdWorker;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @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 BlockingQueue<VoucherOrder> orderTasks =new ArrayBlockingQueue<>(1024 * 1024);
    private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();

    //在类初始化之后执行,因为当这个类初始化好了之后,随时都是有可能要执行的
    @PostConstruct
    private void init() 
        SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
    

    // 用于线程池处理的任务
    // 当初始化完毕后 就会去从对列中去拿信息
    private class VoucherOrderHandler implements Runnable 

        @Override
        public void run() 
            while (true)
                try 
                    // 1.获取队列中的订单信息
                    VoucherOrder voucherOrder = orderTasks.take();
                    // 2.创建订单
                    handleVoucherOrder(voucherOrder);
                 catch (Exception e) 
                    log.error("处理订单异常", e);
                
            
        
    

    private void handleVoucherOrder(VoucherOrder voucherOrder) 
        //1.获取用户
        Long userId = voucherOrder.getUserId();
        // 2.创建锁对象
        RLock lock = redissonClient.getLock("lock:order:" + userId);
        // 3.尝试获取锁
        boolean isLock = lock.tryLock();
        // 4.判断是否获得锁成功
        if (!isLock) 
            // 获取锁失败,直接返回失败或者重试
            log.error("不允许重复下单!");
            return;
        
        try 
            //注意:由于是spring的事务是放在threadLocal中,此时的是多线程,事务会失效
            proxy.createVoucherOrder(voucherOrder);
         finally 
            // 释放锁
            lock.unlock();
        
    

    // 代理对象
    private IVoucherOrderService proxy;

    @Override
    public Result seckillVoucher(Long voucherId) 
        //获取用户
        Long userId = UserHolder.getUser().getId();
        long orderId = redisIdWorker.nextId("order");
        // 1.执行lua脚本
        Long result = stringRedisTemplate.execute(
                SECKILL_SCRIPT,
                Collections.emptyList(),
                voucherId.toString(), userId.toString(), String.valueOf(orderId)
        );
        int r = result.intValue(); // 转成int
        // 2.判断结果是否为0
        if (r != 0) 
            // 2.1.不为0 ,代表没有购买资格
            return Result.fail(r == 1 ? "库存不足" : "不能重复下单");
        
        // 2.2 有购买的资格,创建订单放入阻塞队列中
        VoucherOrder voucherOrder = new VoucherOrder();
        // 2.3.订单id
        voucherOrder.setId(orderId);
        // 2.4.用户id
        voucherOrder.setUserId(userId);
        // 2.5.代金券id
        voucherOrder.setVoucherId(voucherId);
        // 2.6.放入阻塞队列
        orderTasks.add(voucherOrder);
        //3.获取代理对象
        proxy = (IVoucherOrderService)AopContext.currentProxy();
        //4.返回订单id
        return Result.ok(orderId);
    

    @Transactional
    public void createVoucherOrder (VoucherOrder voucherOrder)
        // 5.一人一单逻辑
        // 5.1.用户id
        Long userId = voucherOrder.getUserId();

        // 判断是否存在
        int count = query().eq("user_id", userId)
                .eq("voucher_id", voucherOrder.getId()).count();

        // 5.2.判断是否存在
        if (count > 0) 
            // 用户已经购买过了
            log.error("用户已经购买过了");
        

        //6,扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock= stock -1") //set stock = stock -1
                .eq("voucher_id", voucherOrder.getVoucherId()).gt("stock",0).update(); //where id = ? and stock > 0
        // .eq("voucher_id", voucherId).eq("stock",voucher.getStock()).update(); //where id = ? and stock = ?

        if (!success) 
            //扣减库存
            log.error("库存不足!");
        

        save(voucherOrder);
    



秒杀业务的优化思路是什么?

阻塞队列的异步秒杀 存在哪些问题?


以上是关于优惠卷秒杀系统设计秒杀优化 —— 基于阻塞队实现异步秒杀优化 及 基于Lua脚本判断秒杀库存一人一单的主要内容,如果未能解决你的问题,请参考以下文章

Redis之秒杀下单优化以及认识redis消息队列

Redis之秒杀下单优化以及认识redis消息队列

Redis进阶学习03---Redis完成秒杀和Redis分布式锁的应用

Redis进阶学习04---秒杀优化和消息队列

秒杀系统优化方案(下)吐血整理

我就是因为会秒杀系统,直接涨薪1W