当前位置: 首页 > news >正文

Redis项目:秒杀业务(优化)

当用户发起请求,此时会请求nginx,nginx会访问到tomcat,而tomcat中的程序,会进行串行操作,分成如下几个步骤

1、查询优惠卷

2、判断秒杀库存是否足够

3、查询订单

4、校验是否是一人一单

5、扣减库存

6、创建订单

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

思路:

当用户下单之后,判断库存是否充足只需要到redis中去根据key找对应的value是否大于0即可,如果不充足,则直接结束,如果充足,继续在redis中判断用户是否可以下单,如果set集合中没有这条数据,说明他可以下单,如果set集合中没有这条记录,则将userId和优惠卷存入到redis中,并且返回0,整个过程需要保证是原子性的,我们可以使用lua来操作

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

执行lua脚本 用redis判断购买资格  0则表示用户有资格 消息已经发出

-- 1、参数列表
-- 优惠卷id
local voucherId = ARGV[1]
-- 用户id
local userId = ARGV[2]
-- 订单id
local orderId = ARGV[3]

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

-- 3、脚本业务
-- 获取库存
local stock = redis.call('get', stockKey)
-- 判断库存是否存在或是否为空
if (stock == nil or stock == '') then
    -- 库存不存在或为空,返回错误码(例如 3 表示库存未初始化)
    return 1
end
-- 将库存转换为数字
local stockNum = tonumber(stock)
-- 判断库存是否为有效数字
if (stockNum == nil) then
    -- 库存不是有效数字,返回错误码(例如 4 表示库存数据异常)
    return 1
end
-- 判断库存充足与否
if (stockNum <= 0) then
    -- 库存不足
    return 1
end
-- 判断用户是否下单
if (redis.call('sismember', orderKey, userId) == 1) then
    -- 重复下单
    return 2
end
-- 扣库存 保存用户
redis.call('incrby', stockKey, -1)
redis.call('sadd', orderKey, userId)
-- 发送消息到队列中
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
return 0
public Result seckillVoucher(Long voucherId) {
        //获取用户 线程
        Long userId = UserHolder.getUser().getId();
        //执行LUA脚本
        Long result = stringRedisTemplate.execute(
                SECKILL_SCRIPY,
                Collections.emptyList(),
                voucherId.toString(), userId.toString()
        );
        int r = result.intValue();
        //判断结果是不是0
        if (r != 0) {
            //不是0 代表没有购买资格
            return Result.fail(r == 1 ? "库存不足" : "不能重复下单");
        }
        //是0 有购买资格,保存下单消息到阻塞队列中
        // TODO 保存阻塞队列
        VoucherOrder voucherOrder = new VoucherOrder();
        // 订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 用户id
        voucherOrder.setUserId(userId);
        // 代金券id
        voucherOrder.setVoucherId(voucherId);
        //创建阻塞队列
        orderTasks.add(voucherOrder);
        //获取代理对象 放入队列
         proxy = (IVoucherOrderService) AopContext.currentProxy();
        //返回订单id
        return Result.ok(orderId);
    }*

优化:基于阻塞队列实现秒杀优化

//异步处理线程池
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 redisLock = redissonClient.getLock("lock:order:" + userId);
            // 3.尝试获取锁
            boolean isLock = redisLock.lock();
            // 4.判断是否获得锁成功
            if (!isLock) {
                // 获取锁失败,直接返回失败或者重试
                log.error("不允许重复下单!");
                return;
            }
            try {
				//注意:由于是spring的事务是放在threadLocal中,此时的是多线程,事务会失效
                proxy.createVoucherOrder(voucherOrder);
            } finally {
                // 释放锁
                redisLock.unlock();
            }
    }
     //a
	private BlockingQueue<VoucherOrder> orderTasks =new  ArrayBlockingQueue<>(1024 * 1024);

    @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();
        // 2.判断结果是否为0
        if (r != 0) {
            // 2.1.不为0 ,代表没有购买资格
            return Result.fail(r == 1 ? "库存不足" : "不能重复下单");
        }
        VoucherOrder voucherOrder = new VoucherOrder();
        // 2.3.订单id
        long orderId = redisIdWorker.nextId("order");
        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) {
        Long userId = voucherOrder.getUserId();
        // 5.1.查询订单
        int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();
        // 5.2.判断是否存在
        if (count > 0) {
            // 用户已经购买过了
           log.error("用户已经购买过了");
           return ;
        }

        // 6.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1") // set stock = stock - 1
                .eq("voucher_id", voucherOrder.getVoucherId()).gt("stock", 0) // where id = ? and stock > 0
                .update();
        if (!success) {
            // 扣减失败
            log.error("库存不足");
            return ;
        }
        save(voucherOrder);
 
    }

代码逻辑及实现

1、使用 Executors.newSingleThreadExecutor() 创建一个单线程的线程池,用于异步处理订单

2、执行lua 判断是不是满足要求 满足要求 提交到线程池中 
         proxy = (IVoucherOrderService)AopContext.currentProxy();获取代理 存储到成员变量中,以便后续在异步线程中使用。

3、返回给用户已经完成

4、之后的就是在线程池中完成
        int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count(); 这里做数据库二次检查,确保数据一致性(Redis 和数据库可能不同步) 

总结思路

  • 先利用Redis完成库存余量、一人一单判断,完成抢单业务

  • 再将下单业务放入阻塞队列,利用独立线程异步下单

相关文章:

  • 知识蒸馏:让大模型“瘦身”的魔法
  • LiteratureReading:[2016] Enriching Word Vectors with Subword Information
  • Mac:Maven 下载+安装+环境配置(详细讲解)
  • 过往记录系列 篇四:年报月行情历史梳理
  • std::expected
  • 深度学习 第4章 数值计算和 Deepseek 的实践
  • 【初学者】怎样学习、使用与研究算法?
  • 阅读《Vue.js设计与实现》 -- 02
  • 【Notepad】Notepad优化笔记AutoHotkey语法高亮\设置替换默认的notepad程序\设置主题\增加返回上一个编辑地方插件
  • Android 12系统源码_系统启动(一)init进程
  • 配置阿里云yum源
  • 算法模型从入门到起飞系列——深度优先遍历(DFS)
  • 数据无忧:自动备份策略全解析
  • Java 集合框架
  • 基于FPGA的DDS连续FFT 仿真验证
  • Ubuntu Qt: no service found for - “org.qt-project.qt.mediaplayer“
  • 第14周-Seq2Seq模型-NLP
  • 新型教材≠免设计课程
  • yt-dlp工具下载视频使用方法
  • VMware上调整centos终端的背景颜色
  • 陈文清:推进扫黑除恶常态化走深走实,有力回应人民群众对安居乐业的新期待
  • 总书记考察的上海“模速空间”,要打造什么样的“全球最大”?
  • 美国通过《删除法案》:打击未经同意发布他人私密图像,包括“深度伪造”
  • 发挥全国劳模示范引领作用,加速汽车产业电智化转型
  • 马上评丨又见酒店坐地起价,“老毛病”不能惯着
  • 海尔·2025青岛马拉松两选手被终身禁赛:违规转让号码、穿戴他人号码