原代码实现
@Override
public Result seckillVoucher(Long voucherId) throws InterruptedException {
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
if (seckillVoucher == null) {
return Result.fail("优惠卷不存在");
}
// 判断秒杀时间
LocalDateTime now = LocalDateTime.now();
if(seckillVoucher.getBeginTime().isAfter(now)){
return Result.fail("秒杀尚未开始");
}
if(seckillVoucher.getEndTime().isBefore(now)){
return Result.fail("秒杀已经结束");
}
// 查询库存
if(seckillVoucher.getStock()<0){
return Result.fail("库存不足");
}
// 提取一人一单,扣减库存,创建订单的代码加锁
Long userId = UserHolder.getUser().getId();
RLock lock = redissonClient.getLock("shop:" + userId.toString());
boolean isLock = lock.tryLock(5, 10 , TimeUnit.SECONDS);
if (!isLock) {
return Result.fail("不允许重复下单");
}
try {
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId, userId);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
@Transactional
public Result createVoucherOrder(Long voucherId, Long userId) {
Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
return Result.fail("已经购买过了");
}
// 扣减库存
boolean isSuccess = seckillVoucherService.update().setSql("stock = stock - 1")
.eq("voucher_id", voucherId).gt("stock", 0).update();
if (!isSuccess) {
return Result.fail("库存不足");
}
// 创建订单
long id = idWorker.nextId("seckillVoucherOrder");
VoucherOrder voucherOrder = new VoucherOrder();
voucherOrder.setId(id);
voucherOrder.setVoucherId(voucherId);
voucherOrder.setUserId(userId);
voucherOrder.setCreateTime(LocalDateTime.now());
save(voucherOrder);
return Result.ok(id);
}
痛点:
- 数据库压力:频繁查询库存和订单导致数据库负载过高。
- 响应延迟:同步操作(如事务提交)阻塞用户请求。
需求分析
减少数据库访问:通过设置缓存,将优惠卷数量,下单用户存入缓存。
性能提升:开启异步消费者线程处理添加订单逻辑提高吞吐量。
实现过程
一、新增秒杀优惠卷的同时将优惠卷信息保存在Redis中
@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.getStock().toString());
}
二、基于Lua脚本,判断秒杀库存,一人一单,决定用户是否抢购成功
Lua脚本实现代码seckill.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)
Spring Data Redis定义Lua脚本
public static final DefaultRedisScript<Long> SECKILL_SCRIPT;
static {
SECKILL_SCRIPT = new DefaultRedisScript<>();
SECKILL_SCRIPT.setLocation(new ClassPathResource("./lua/seckill.lua"));
SECKILL_SCRIPT.setResultType(Long.class);
}
三、如果抢购成功,将优惠卷id和用户id封装后存入阻塞队列
创建阻塞队列
public BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024*1024);
抢购成功加入阻塞队列
@Override
public Result seckillVoucher(Long voucherId) {
// 1.执行Lua脚本
long result = stringRedisTemplate.execute(
SECKILL_SCRIPT,
Collections.emptyList(),
voucherId.toString(),
UserHolder.getUser().getId().toString()
);
// 2.结果为1,库存不足
if(result == 1){
return Result.fail("库存不足");
}
// 3.结果为2,重复下单
if(result == 2){
return Result.fail("不允许重复下单");
}
// 4.库存为0,将订单信息保存到阻塞队列
VoucherOrder voucherOrder = new VoucherOrder();
voucherOrder.setVoucherId(voucherId);
voucherOrder.setUserId(UserHolder.getUser().getId());
long id = idWorker.nextId("seckillVoucherOrder");
voucherOrder.setId(id);
orderTasks.add(voucherOrder);
// 5.返回订单id
return Result.ok(id);
}
四、开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能
public BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024*1024);
public static final ExecutorService SECKILL_ORDER_EXECUTER = Executors.newSingleThreadExecutor();
@PostConstruct
private void init(){
SECKILL_ORDER_EXECUTER.submit(new voucherOrderHandler());
}
private class voucherOrderHandler implements Runnable {
@Override
public void run() {
while(true){
try {
VoucherOrder order = orderTasks.take();
handleVoucherOrder(order);
} catch (Exception e) {
log.error("处理订单异常", e);
}
}
}
}
private void handleVoucherOrder(VoucherOrder order) throws InterruptedException {
// 提取一人一单,扣减库存,创建订单的代码加锁
Long userId = order.getUserId();
// 创建锁对象
RLock lock = redissonClient.getLock("shop:" + userId.toString());
// 尝试获取锁
boolean isLock = lock.tryLock(5, 10, TimeUnit.SECONDS);
if(!isLock){
return;
}
try {
proxy.createVoucherOrder(order);
} catch (IllegalStateException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
异步线程中代理对象获取的关键问题分析
异步线程(voucherOrderHandler
)尝试通过 AopContext.currentProxy()
获取代理对象 proxy
,但由于 线程上下文隔离 和 AOP 代理机制限制,该操作在异步线程中 无法成功。以下是具体原因和解决方案:
一、问题根源
-
AopContext 的 ThreadLocal 特性
AopContext.currentProxy()
底层依赖ThreadLocal
存储当前线程的代理对象。- 主线程(HTTP 请求线程)中调用VoucherOrderService,Spring Aop拦截器将代理对象写入ThreadLocal,然后执行原方法,在原方法中就可以通过调用 AopContext.currentProxy()就可以获取当前类的代理对象。
-
异步线程中 ThreadLocal 为空的原因
当异步线程处理订单时,调用 handleVoucherOrder
方法:
private void handleVoucherOrder(VoucherOrder order) {
// 尝试获取代理对象(失败!)
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
}
- 线程隔离性:异步线程的
ThreadLocal
是独立存储空间,主线程的ThreadLocal
数据不会自动传递。 - 未触发代理机制:异步线程未通过代理对象调用方法,因此 Spring 未将代理对象存入其
ThreadLocal
。
二、解决方案
方案 1:传递代理对象到异步任务(推荐)
在提交订单到队列时,将当前代理对象与订单对象绑定:
// 修改订单队列类型
public BlockingQueue<OrderTask> orderTasks = new ArrayBlockingQueue<>(1024*1024);
// 定义订单任务包装类
public class OrderTask {
private VoucherOrder order;
private IVoucherOrderService proxy;
public OrderTask(VoucherOrder order, IVoucherOrderService proxy) {
this.order = order;
this.proxy = proxy;
}
// Getter 省略
}
// 修改秒杀方法中的任务提交逻辑
@Override
public Result seckillVoucher(Long voucherId) {
// ... 省略其他逻辑 ...
// 获取当前代理对象(主线程中)
IVoucherOrderService currentProxy = (IVoucherOrderService) AopContext.currentProxy();
// 提交任务时绑定代理对象
orderTasks.add(new OrderTask(voucherOrder, currentProxy));
return Result.ok(id);
}
// 修改异步任务处理逻辑
private class voucherOrderHandler implements Runnable {
@Override
public void run() {
while (true) {
try {
OrderTask task = orderTasks.take();
handleVoucherOrder(task.getOrder(), task.getProxy());
} catch (Exception e) {
log.error("处理订单异常", e);
}
}
}
}
private void handleVoucherOrder(VoucherOrder order, IVoucherOrderService proxy) {
RLock lock = redissonClient.getLock("shop:" + order.getUserId());
try {
if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
proxy.createVoucherOrder(order); // 使用传递的代理对象
}
} finally {
lock.unlock();
}
}
方案 2:通过自注入获取代理对象
让 Spring 自动注入代理对象,避免依赖 AopContext
:
@Service
public class VoucherOrderServiceImpl implements IVoucherOrderService {
@Autowired
private IVoucherOrderService selfProxy; // Spring 会自动注入代理对象
private void handleVoucherOrder(VoucherOrder order) {
RLock lock = redissonClient.getLock("shop:" + order.getUserId());
try {
if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
selfProxy.createVoucherOrder(order); // 直接使用注入的代理对象
}
} finally {
lock.unlock();
}
}
}
方案 3:显式从 Spring 容器获取代理
通过 ApplicationContextAware
接口获取代理对象:
@Service
public class VoucherOrderServiceImpl implements IVoucherOrderService, ApplicationContextAware {
private ApplicationContext applicationContext;
private IVoucherOrderService proxy;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
this.proxy = applicationContext.getBean(IVoucherOrderService.class);
}
private void handleVoucherOrder(VoucherOrder order) {
RLock lock = redissonClient.getLock("shop:" + order.getUserId());
try {
if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
proxy.createVoucherOrder(order); // 使用容器中的代理对象
}
} finally {
lock.unlock();
}
}
}
存在的问题
-
内存限制问题
阻塞队列的容量是有限的,如果秒杀请求量过大,队列可能被填满,后续请求无法加入队列,导致部分用户请求失败。
-
数据安全问题
如果消费者进程在异步处理的过程中宕机,阻塞队列中的其他订单将会丢失,导致剩余订单无法处理。
现存问题与局限性
问题 | 影响 | 临时解决方案 |
---|---|---|
阻塞队列容量限制 | 高并发时队列溢出导致请求丢失 | 增大队列容量 |
数据可靠性不足 | 进程宕机导致未处理订单丢失 | 定期持久化队列数据到数据库 |
扩展性受限 | 单线程消费无法应对突发流量 | 增加消费者线程数 |
改进方向
我们可以使用消息队列处理以上问题。
消息队列(Message Queue),字面意思就是存放消息的队列。最简单的消息队列模型包括3个角色:
消息队列:存储和管理消息,也被称为消息代理(Message Broker)
生产者:发送消息到消息队列
消费者:从消息队列获取消息并处理消息
下篇文章我们使用Redis完成消息队列。