Redis 优化秒杀(异步秒杀)

目录

为什么需要异步秒杀

异步优化的核心逻辑是什么?

阻塞队列的特点是什么?

Lua脚本在这里的作用是什么?

异步调用创建订单的具体逻辑是什么?

为什么要用代理对象proxy调用createVoucherOrder方法?

对于代码的详细解释:

[SECKILL_ORDER_EXECUTOR 是什么?](#SECKILL_ORDER_EXECUTOR 是什么?)

[@PostConstruct 是什么?](#@PostConstruct 是什么?)

[VoucherOrderHandler 是什么?](#VoucherOrderHandler 是什么?)

[VoucherOrderHandler 调用的handleVoucherOrder:](#VoucherOrderHandler 调用的handleVoucherOrder:)

数据库操作的注意点有哪些?

[seckillVoucher 方法:](#seckillVoucher 方法:)

[单线程线程池、阻塞队列、seckillVoucher 和 VoucherOrderHandler 的协作过程总结](#单线程线程池、阻塞队列、seckillVoucher 和 VoucherOrderHandler 的协作过程总结)

方法调用流程总览

方法逻辑一览表

完整代码


在秒杀场景中,我们可以将库存存入 Redis,并通过 Lua 脚本来判断用户是否有秒杀资格,同时实现一人一单的限制。由于 Redis 的单线程特性和 Lua 脚本的原子性保障,能够避免多个线程交叉执行 Redis 命令导致的并发问题。同时,使用阻塞队列将订单请求进行缓冲,当线程尝试从队列中获取订单时,如果队列为空,线程会被阻塞,直到有新订单加入队列,线程才会被唤醒并处理订单,从而实现高效的生产者-消费者模型。

为什么需要异步秒杀

1. 防止数据库压力过载

  • 异步秒杀通过将订单请求写入阻塞队列,削峰填谷,避免将瞬时高并发请求直接传递到数据库。
  • 消费者线程从队列中按顺序取出订单进行处理,减少数据库同时处理的请求量。

2. 提升系统响应速度

  • 秒杀请求在异步架构中:
    1. 同步部分:快速返回秒杀结果(例如秒杀资格校验)。
    2. 异步部分:订单的具体处理(如扣减库存、保存订单)放到后台处理。
  • 这种分离让用户能快速得到响应,而系统后台有更多时间处理复杂的订单逻辑。

异步优化的核心逻辑是什么?

问:为什么需要异步优化秒杀订单? 答:在高并发场景中,秒杀会同时产生大量订单请求。如果直接将请求交给数据库处理,容易导致数据库压力过大,从而系统崩溃。异步优化通过使用阻塞队列将订单请求排队,避免直接对数据库产生瞬时高负载。

问:如何实现异步处理? 答:**将订单信息保存到阻塞队列中,使用单线程(线程池中的线程)从队列中按顺序取出订单进行处理。**这样可以削峰填谷,减轻数据库压力。


阻塞队列的特点是什么?

问:阻塞队列的作用是什么? 答:阻塞队列是线程安全的队列,支持生产者-消费者模型**。在代码中,生产者是seckillVoucher方法,它将订单信息加入阻塞队列;消费者是VoucherOrderHandler线程,它从队列中取出订单进行处理。**

问:为什么使用阻塞队列? 答:阻塞队列的特点是,如果队列为空,消费者线程会阻塞等待;如果队列满了,生产者线程会阻塞等待。这样可以很好地协调生产者和消费者的速度,避免资源浪费或超负荷。


Lua脚本在这里的作用是什么?

问:为什么使用Lua脚本操作Redis? 答:Lua脚本在Redis中是原子执行的。使用Lua脚本可以保证秒杀资格验证和库存扣减的原子性,避免并发问题。

问:Lua脚本验证了什么? 答:

  1. 用户是否重复下单(通过Redis中存储的用户信息判断)。
  2. 秒杀库存是否充足(通过Redis中存储的库存数量判断)。
Lua 复制代码
-- 参数
-- 优惠券id
local voucherId = ARGV[1]
-- 用户id
local userId = ARGV[2]

-- 数据key
local stockKey = 'seckill:stock:'.. voucherId
local orderKey = 'seckill: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)
redis.call('sadd', orderKey, userId)
return 0

将秒杀券的库存以String形式存入Redis

java 复制代码
 @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当中
        stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(),voucher.getStock().toString());
    }

异步调用创建订单的具体逻辑是什么?

java 复制代码
@Override
public Result seckillVoucher(Long voucherId) {
    Long userId = UserHolder.getUser().getId();

    // 1. 校验秒杀资格
    Long res = stringRedisTemplate.execute(
        SECKILL_SCRIPT,
        Collections.emptyList(),
        voucherId.toString(),
        userId.toString()
    );

    if (res != 0) {
        // 秒杀资格校验失败
        return Result.fail(res == 1 ? "库存不足" : "重复下单");
    }

    // 2. 生成订单信息
    VoucherOrder voucherOrder = new VoucherOrder();
    long orderID = redisIdWorker.nextId("order");
    voucherOrder.setId(orderID);
    voucherOrder.setUserId(userId);
    voucherOrder.setVoucherId(voucherId);

    // 3. 将订单信息放入阻塞队列
    orderTasks.add(voucherOrder);

    // 获取代理对象
    proxy = (IVoucherOrderService) AopContext.currentProxy();

    return Result.ok(orderID);
}

问:seckillVoucher方法中发生了什么? 答:这是异步调用的入口逻辑,分为以下几个步骤:

  1. 验证秒杀资格
    • 使用Lua脚本操作Redis,确保原子性。
    • 判断用户是否重复下单,或者库存是否不足。
    • 如果秒杀资格验证失败,则直接返回错误信息。
  2. 生成订单信息
    • 使用RedisIdWorker生成订单ID。
    • 将订单信息(用户ID、代金券ID等)封装成VoucherOrder对象。
  3. 将订单信息保存到阻塞队列
    • 调用orderTasks.add(voucherOrder)将订单加入阻塞队列中。
  4. 返回订单ID
    • 在返回给用户订单ID时,并没有真正完成订单,而是进入队列等待处理。

为什么要用代理对象proxy调用createVoucherOrder方法?

问:为什么不直接调用createVoucherOrder

答:因为 createVoucherOrder 方法是事务方法,需要通过代理对象调用才能生效。

  1. Spring 的事务机制基于 AOP(面向切面编程)实现

    • Spring 使用代理对象(动态代理或 CGLIB 代理)来拦截对事务方法的调用,并在方法执行前后添加事务管理逻辑(如开启事务、提交事务或回滚事务)。
    • 如果直接调用类内部的事务方法,调用不会经过代理对象,而是直接执行原始方法,Spring 的事务管理器无法介入,导致事务逻辑失效。
  2. 内部调用的问题

    • 在类的内部直接调用另一个事务方法时,调用不会经过代理对象,而是通过 this 调用,因此事务拦截器不会生效,事务注解(@Transactional)失效。

问:代理对象是如何获取的?

  • 将代理对象声明为一个成员变量,通过 AopContext.currentProxy() 获取当前类的代理对象。
  • 原因AopContext.currentProxy() 返回的是 Spring AOP 生成的当前类的代理对象,它能够拦截方法调用,从而触发事务管理逻辑。
  • 在异步线程中直接调用当前类的方法时,事务不会生效,因为直接调用是通过 this 引用,而不是代理对象调用。通过成员变量保存的代理对象,即使在异步线程中调用方法,也可以确保事务逻辑有效。
  • 最终,通过代理对象调用 createVoucherOrder 方法,可以正常触发 Spring 的事务管理器,确保事务功能生效。

对于代码的详细解释:

java 复制代码
private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();

private final BlockingQueue<VoucherOrder> orderTasks = new LinkedBlockingQueue<>();
@PostConstruct
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("Error processing order", e);
              }
          }
      }
  }

SECKILL_ORDER_EXECUTOR 是什么?

SECKILL_ORDER_EXECUTOR 是一个 单线程线程池,用来处理秒杀订单的异步任务。

java 复制代码
private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();

单线程线程池的特点是:线程池中始终只有一个线程,任务会按顺序执行,适合需要顺序处理的场景。

它的主要作用是 管理和调度线程的生命周期。具体来说:

启动和管理消费者线程

  • VoucherOrderHandler 需要一个线程不断运行,用来从阻塞队列中取订单并处理。
  • 线程池 SECKILL_ORDER_EXECUTOR 的作用是启动这个线程,并保证这个线程的生命周期由线程池管理。

线程复用

  • 如果你手动创建线程(new Thread()),可能会导致系统频繁创建和销毁线程,浪费系统资源。
  • 使用线程池可以复用线程,减少线程的创建和销毁开销,提高性能。

稳定性

  • 如果 VoucherOrderHandler 线程在执行中意外退出(例如抛出未捕获异常),线程池会自动接管并重新启动线程,保证任务不会中断。

在这里,SECKILL_ORDER_EXECUTOR 通过单线程的方式从阻塞队列中取出订单,按顺序处理,确保秒杀订单的处理逻辑是线程安全的。


@PostConstruct 是什么?

@PostConstruct 是 Java 的一个注解,作用是在 Spring 容器将 Bean 初始化完成后 ,立即执行标注的方法。换句话说,当 Spring 加载并创建了 VoucherOrderServiceImpl 实例后,会自动调用 init() 方法。

这是一个生命周期回调方法,常用于初始化逻辑,比如启动线程、加载配置等。


init() 方法的作用是什么?

  • 这个方法的主要作用是 启动一个专用线程(由单线程线程池管理),用于从阻塞队列中取出订单并进行异步处理。
  • 通过 SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());,将 VoucherOrderHandler 提交到线程池中,线程池会启动一个线程,持续运行 VoucherOrderHandler 中的逻辑。

VoucherOrderHandler 是什么?

VoucherOrderHandler 是一个内部类,它实现了 Runnable 接口,代表一个任务。

  • 任务的核心逻辑是:从阻塞队列中取出订单并处理
  • 它的 run() 方法包含一个 while(true) 循环,这样线程会一直运行,不断从队列中取出订单(通过 orderTasks.take()),直到程序终止。

**VoucherOrderHandler**调用的handleVoucherOrder:

  • 防止同一用户多次下单(重复下单)。

  • 调用执行订单的具体业务逻辑的方法createVoucherOrder
    (如扣减库存、保存订单等)。

    java 复制代码
        @Transactional
        public void createVoucherOrder(VoucherOrder voucherOrder) {
            // 实现一人一单,我们需要先判断该用户是否已经抢过了
            // 根据优惠券id和用户id查询订单
            Long userId = UserHolder.getUser().getId();
            int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder).count();
            if (count > 0) {
               log.error("已经购买过,不可重复购买!");
            }
            // 扣减库存
            boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherOrder).
    //                eq("stock",voucher.getStock()). // 加个乐观锁,如果现在的库存和我之前查询的库存不相同,说明在我之前就有线程修改了数据库
            gt("stock", 0).update();
            if (!success) {
                log.error("库存不足!");
            }
            // 写入数据库 不需要再返回orderId了,因为之前在seckillVoucher已经返回了
            save(voucherOrder);
        }

数据库操作的注意点有哪些?

问:如何实现一人一单的限制? 答:在createVoucherOrder方法中,通过查询数据库判断用户是否已经购买过对应的代金券。

问:如何扣减库存? 答:使用seckillVoucherService执行SQL语句更新库存,并通过gt(\"stock\", 0)确保库存大于0。


整体逻辑总结

  • 触发时机 : 当 VoucherOrderServiceImpl 被 Spring 加载并实例化后,@PostConstruct 注解标注的 init() 方法会被调用。

  • 作用init() 方法向线程池提交了一个 VoucherOrderHandler 任务,这个任务会启动一个线程,不断从阻塞队列中取出订单并调用相关处理逻辑(handleVoucherOrder)。


seckillVoucher 方法:

单线程线程池、阻塞队列、 seckillVoucher VoucherOrderHandler 的协作过程总结

seckillVoucher 是厨师

  • 它负责接收顾客的订单请求(秒杀请求),检查是否符合要求(库存是否足够、是否重复下单),然后生成订单(菜品)并放在桌子上(阻塞队列)。
  • 核心职责:生产订单,确保每个订单合法并生成完整订单信息。

BlockingQueue 是桌子

  • 它负责临时存放厨师制作好的订单(菜品),保证每个订单都按顺序排列。
  • 如果桌子空了,顾客(消费者线程)只能等;如果桌子满了,厨师(生产者线程)也需要暂停制作。
  • 核心职责:缓冲区,用于在生产和消费之间解耦。

VoucherOrderHandler 是顾客

  • 它负责从桌子上取菜(从队列中取订单),并最终消费(处理订单,包括扣减库存、写入数据库等)。
  • 如果桌子没有菜了,它会耐心等待;一旦有菜,它会立刻取走并处理。
  • 核心职责:消费订单,执行订单处理逻辑。

SECKILL_ORDER_EXECUTOR 是服务员

  • 它负责启动和管理顾客(消费者线程),确保顾客始终在桌子旁边等待取菜。
  • 如果顾客突然有事不要菜品了(比如异常退出),服务员会招待一个新的顾客来接替。
  • 核心职责:管理消费者线程的生命周期,确保订单处理不断运行。

方法调用流程总览

  1. 用户发起秒杀请求,触发 seckillVoucher 方法。
  2. seckillVoucher 验证秒杀资格并将订单放入阻塞队列。
  3. VoucherOrderHandler(由线程池管理的消费者线程)从队列中取出订单,调用 handleVoucherOrder 进行处理。
  4. handleVoucherOrder 利用分布式锁防止重复下单,并调用 createVoucherOrder 完成订单的核心逻辑。
  5. createVoucherOrder 执行订单的最终处理,包括扣减库存、写入数据库等。

方法逻辑一览表

方法 作用 关键逻辑
seckillVoucher 秒杀请求入口,生成订单并加入阻塞队列 验证秒杀资格,生成订单信息,加入阻塞队列。
阻塞队列 (BlockingQueue) 存储订单信息,实现生产者与消费者的解耦 线程安全存储,缓冲生产者和消费者速度差异。
VoucherOrderHandler 消费者线程,从队列中取订单并调用处理方法 从队列取订单,调用 handleVoucherOrder
handleVoucherOrder 防止重复下单,调用核心业务逻辑 创建分布式锁,防止重复下单,调用 createVoucherOrder
createVoucherOrder 执行订单的核心逻辑 校验订单、扣减库存、保存订单到数据库。

完整代码

java 复制代码
@Service
@RequiredArgsConstructor
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
    @Resource
    private ISeckillVoucherService seckillVoucherService;
    final RedisIdWorker redisIdWorker;
    final StringRedisTemplate stringRedisTemplate;
    final 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 static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
    private IVoucherOrderService proxy;
    private final BlockingQueue<VoucherOrder> orderTasks = new LinkedBlockingQueue<>();

    @PostConstruct
    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("Error processing order", e);
                }
            }
        }
    }

    private void handleVoucherOrder(VoucherOrder voucherOrder) {
        Long userId = voucherOrder.getUserId();

        // 1. Create lock
        RLock lock = redissonClient.getLock("lock:order:" + userId);

        // 2. Try to acquire lock
        boolean isLock = lock.tryLock();
        if (!isLock) {
            log.error("Duplicate order not allowed");
            return;
        }

        try {
            // 3. Create order via proxy
            proxy.createVoucherOrder(voucherOrder);
        } finally {
            // 4. Release lock
            lock.unlock();
        }
    }

    /**
     * 基于异步Lua脚本保证原子性
     *
     * @param voucherId
     * @return
     */
    @Override
    public Result seckillVoucher(Long voucherId) {
        Long userId = UserHolder.getUser().getId();
        // 执行Lua脚本
        Long res = stringRedisTemplate.execute(
                SECKILL_SCRIPT,
                Collections.emptyList(),
                voucherId.toString(),
                userId.toString()
        );
        // 判断返回值是否为0
        if (res != 0) {
            // 非0 则没有秒杀资格
            return Result.fail(res == 1 ? "库存不足" : "重复下单");
        }
        // 从Redis当中获取下单信息
        long orderId = redisIdWorker.nextId("order");
        // TODO 为0 表示有秒杀资格 需要将下单信息保存在阻塞队列当中
        // 创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        // 订单id
        long orderID = redisIdWorker.nextId("order");
        voucherOrder.setId(orderID);
        // 用户id
        voucherOrder.setUserId(UserHolder.getUser().getId());
        // 代金券id
        voucherOrder.setVoucherId(voucherId);
        // 保存到阻塞队列当中
        orderTasks.add(voucherOrder);

        // 获取代理对象
        proxy = (IVoucherOrderService) AopContext.currentProxy();
        return Result.ok(orderId);
    }
    
    @Transactional
    public void createVoucherOrder(VoucherOrder voucherOrder) {
        // 实现一人一单,我们需要先判断该用户是否已经抢过了
        // 根据优惠券id和用户id查询订单
        Long userId = UserHolder.getUser().getId();
        int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder).count();
        if (count > 0) {
           log.error("已经购买过,不可重复购买!");
        }
        // 扣减库存
        boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherOrder).
//                eq("stock",voucher.getStock()). // 加个乐观锁,如果现在的库存和我之前查询的库存不相同,说明在我之前就有线程修改了数据库
        gt("stock", 0).update();
        if (!success) {
            log.error("库存不足!");
        }
        // 写入数据库
        save(voucherOrder);
    }
}
相关推荐
MiniFlyZt2 小时前
省市区三级联动(后端)
数据库·spring boot
背太阳的牧羊人2 小时前
用于与多个数据库聊天的智能 SQL 代理问答和 RAG 系统(2) —— 从 PDF 文档生成矢量数据库 (VectorDB),然后存储文本的嵌入向量
数据库·人工智能·sql·langchain·pdf
程序员谷美2 小时前
Redis 性能优化:利用 MGET 和 Pipeline 提升效率
java·redis·性能优化
zhangxueyi3 小时前
MySQL之企业面试题:InnoDB存储引擎组成部分、作用
java·数据库·mysql·面试·innodb
一条小小yu3 小时前
java 从零开始手写 redis(六)redis AOF 持久化原理详解及实现
java·redis·spring
极客先躯3 小时前
Redis 安装与配置指南
数据库·redis·数据验证·安装说明·编译和安装·redis 集群配置·查看集群
小湿哥3 小时前
RedisDB双机主从同步性能测试
redis·nosql·性能测试·同步性能
YaenLi4 小时前
MySQL 安装部署
linux·数据库·mysql
乄北城以北乀4 小时前
一.MySQL程序简介
数据库·mysql