目录
为什么要用代理对象proxy调用createVoucherOrder方法?
VoucherOrderHandler 调用的handleVoucherOrder:
单线程线程池、阻塞队列、seckillVoucher 和 VoucherOrderHandler 的协作过程总结
在秒杀场景中,我们可以将库存存入 Redis,并通过 Lua 脚本来判断用户是否有秒杀资格,同时实现一人一单的限制。由于 Redis 的单线程特性和 Lua 脚本的原子性保障,能够避免多个线程交叉执行 Redis 命令导致的并发问题。同时,使用阻塞队列将订单请求进行缓冲,当线程尝试从队列中获取订单时,如果队列为空,线程会被阻塞,直到有新订单加入队列,线程才会被唤醒并处理订单,从而实现高效的生产者-消费者模型。
为什么需要异步秒杀
1. 防止数据库压力过载
- 异步秒杀通过将订单请求写入阻塞队列,削峰填谷,避免将瞬时高并发请求直接传递到数据库。
- 消费者线程从队列中按顺序取出订单进行处理,减少数据库同时处理的请求量。
2. 提升系统响应速度
- 秒杀请求在异步架构中:
- 同步部分:快速返回秒杀结果(例如秒杀资格校验)。
- 异步部分:订单的具体处理(如扣减库存、保存订单)放到后台处理。
- 这种分离让用户能快速得到响应,而系统后台有更多时间处理复杂的订单逻辑。
异步优化的核心逻辑是什么?
问:为什么需要异步优化秒杀订单? 答:在高并发场景中,秒杀会同时产生大量订单请求。如果直接将请求交给数据库处理,容易导致数据库压力过大,从而系统崩溃。异步优化通过使用阻塞队列将订单请求排队,避免直接对数据库产生瞬时高负载。
问:如何实现异步处理? 答:将订单信息保存到阻塞队列中,使用单线程(线程池中的线程)从队列中按顺序取出订单进行处理。这样可以削峰填谷,减轻数据库压力。
阻塞队列的特点是什么?
问:阻塞队列的作用是什么? 答:阻塞队列是线程安全的队列,支持生产者-消费者模型。在代码中,生产者是seckillVoucher
方法,它将订单信息加入阻塞队列;消费者是VoucherOrderHandler
线程,它从队列中取出订单进行处理。
问:为什么使用阻塞队列? 答:阻塞队列的特点是,如果队列为空,消费者线程会阻塞等待;如果队列满了,生产者线程会阻塞等待。这样可以很好地协调生产者和消费者的速度,避免资源浪费或超负荷。
Lua脚本在这里的作用是什么?
问:为什么使用Lua脚本操作Redis? 答:Lua脚本在Redis中是原子执行的。使用Lua脚本可以保证秒杀资格验证和库存扣减的原子性,避免并发问题。
问:Lua脚本验证了什么? 答:
- 用户是否重复下单(通过Redis中存储的用户信息判断)。
- 秒杀库存是否充足(通过Redis中存储的库存数量判断)。
-- 参数
-- 优惠券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
@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());
}
异步调用创建订单的具体逻辑是什么?
@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
方法中发生了什么? 答:这是异步调用的入口逻辑,分为以下几个步骤:
- 验证秒杀资格:
- 使用Lua脚本操作Redis,确保原子性。
- 判断用户是否重复下单,或者库存是否不足。
- 如果秒杀资格验证失败,则直接返回错误信息。
- 生成订单信息:
- 使用
RedisIdWorker
生成订单ID。 - 将订单信息(用户ID、代金券ID等)封装成
VoucherOrder
对象。
- 使用
- 将订单信息保存到阻塞队列:
- 调用
orderTasks.add(voucherOrder)
将订单加入阻塞队列中。
- 调用
- 返回订单ID:
- 在返回给用户订单ID时,并没有真正完成订单,而是进入队列等待处理。
为什么要用代理对象proxy
调用createVoucherOrder
方法?
问:为什么不直接调用createVoucherOrder
?
答:因为 createVoucherOrder
方法是事务方法,需要通过代理对象调用才能生效。
-
Spring 的事务机制基于 AOP(面向切面编程)实现:
- Spring 使用代理对象(动态代理或 CGLIB 代理)来拦截对事务方法的调用,并在方法执行前后添加事务管理逻辑(如开启事务、提交事务或回滚事务)。
- 如果直接调用类内部的事务方法,调用不会经过代理对象,而是直接执行原始方法,Spring 的事务管理器无法介入,导致事务逻辑失效。
-
内部调用的问题:
- 在类的内部直接调用另一个事务方法时,调用不会经过代理对象,而是通过
this
调用,因此事务拦截器不会生效,事务注解(@Transactional
)失效。
- 在类的内部直接调用另一个事务方法时,调用不会经过代理对象,而是通过
问:代理对象是如何获取的?
- 将代理对象声明为一个成员变量,通过
AopContext.currentProxy()
获取当前类的代理对象。 - 原因:
AopContext.currentProxy()
返回的是 Spring AOP 生成的当前类的代理对象,它能够拦截方法调用,从而触发事务管理逻辑。 - 在异步线程中直接调用当前类的方法时,事务不会生效,因为直接调用是通过
this
引用,而不是代理对象调用。通过成员变量保存的代理对象,即使在异步线程中调用方法,也可以确保事务逻辑有效。 - 最终,通过代理对象调用
createVoucherOrder
方法,可以正常触发 Spring 的事务管理器,确保事务功能生效。
对于代码的详细解释:
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
是一个 单线程线程池,用来处理秒杀订单的异步任务。
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
(如扣减库存、保存订单等)。@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
是服务员:
- 它负责启动和管理顾客(消费者线程),确保顾客始终在桌子旁边等待取菜。
- 如果顾客突然有事不要菜品了(比如异常退出),服务员会招待一个新的顾客来接替。
- 核心职责:管理消费者线程的生命周期,确保订单处理不断运行。
方法调用流程总览
- 用户发起秒杀请求,触发
seckillVoucher
方法。 seckillVoucher
验证秒杀资格并将订单放入阻塞队列。VoucherOrderHandler
(由线程池管理的消费者线程)从队列中取出订单,调用handleVoucherOrder
进行处理。handleVoucherOrder
利用分布式锁防止重复下单,并调用createVoucherOrder
完成订单的核心逻辑。createVoucherOrder
执行订单的最终处理,包括扣减库存、写入数据库等。
方法逻辑一览表
方法 | 作用 | 关键逻辑 |
---|---|---|
seckillVoucher | 秒杀请求入口,生成订单并加入阻塞队列 | 验证秒杀资格,生成订单信息,加入阻塞队列。 |
阻塞队列 (BlockingQueue ) | 存储订单信息,实现生产者与消费者的解耦 | 线程安全存储,缓冲生产者和消费者速度差异。 |
VoucherOrderHandler | 消费者线程,从队列中取订单并调用处理方法 | 从队列取订单,调用 handleVoucherOrder 。 |
handleVoucherOrder | 防止重复下单,调用核心业务逻辑 | 创建分布式锁,防止重复下单,调用 createVoucherOrder 。 |
createVoucherOrder | 执行订单的核心逻辑 | 校验订单、扣减库存、保存订单到数据库。 |
完整代码
@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);
}
}