遠くへと広がる海の色暖かく
秒杀优化
判断优惠券库存以及一人一单都涉及到数据库操作,为了提高效率,将数据库操作优化为Redis操作。判断是否可以下单以及下单操作均在Redis中完成,而下单操作会存入异步阻塞队列中,后台服务器单开异步线程读取异步阻塞队列,完成数据库同步。示意图如下:

使用Redis维护每款优惠券库存,使用优惠券Redis中的Set结构维护每款优惠券已经被哪些用户下单,使用Lua脚本保证判断库存是否充足、判断一人一单、扣减库存、将当前用户Id存入Set等一系列操作的原子性。示意图如下:

改进秒杀业务
需求:
①新增秒杀优惠券的同时,将优惠券信息保存到Redis中
②基于Lua脚本,判断秒杀库存、一人一单,决定用户是否抢购成功
③如果抢购成功,将优惠券id和用户id封装后存入阻塞队列
④开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能
新增秒杀优惠券并保存到Redis
修改新增秒杀优惠券代码,新增添加Redis缓存添加操作
@Service
public class VoucherSecondKillServiceImpl implements VoucherSecondKillService {@Autowiredprivate VoucherSecondKillMapper voucherSecondKillMapper;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void AddVoucherSecondKill(VoucherData NewVoucher) {VoucherSecondKillData NewVoucherSecondKillData = new VoucherSecondKillData();NewVoucherSecondKillData.setVoucherId(NewVoucher.getId());NewVoucherSecondKillData.setStock(NewVoucher.getStock());NewVoucherSecondKillData.setBeginTime(NewVoucher.getBeginTime());NewVoucherSecondKillData.setBeginTime(NewVoucher.getEndTime());voucherSecondKillMapper.insert(NewVoucherSecondKillData);// Redis缓存添加操作stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY+NewVoucher.getId(),NewVoucher.getStock().toString());}
}
封装常量工具类
public class RedisConstants {public static final String SECKILL_STOCK_KEY = "seckill:stock:";
}
编写Lua脚本
Lua脚本执行行程图如下:

使用Redis中的sismember操作来判断key对应的set类型value是否存在相关成员
sismember key member


Lua脚本编写如下,其中..是Lua脚本的字符串拼接操作:
local VoucherId = ARGV[1]
local UserId = ARGV[2]local StockKey = 'seckill:stock:' .. VoucherId
local OrderKey = 'seckill:order:' .. VoucherId-- 判断库存是否充足
if(tonumber(redis.call('get',StockKey)) <=0) then-- 库存不足返回1return 1
end-- 判断用户是否下单
if(redis.call('sismember',OrderKey,UserId) ==1) then-- 用户存在说明重复下单,返回2return 2
end-- Redis库存-1
redis.call('incrby',StockKey,-1)
-- Redis下单用户集合添加当前用户
redis.call('sadd',OrderKey,UserId)
-- 成功返回0
return 0;
服务层方法修改如下,其中使用stringRedisTemplate.execute方法调用Lua脚本执行,Objects.requireNonNull().intValue()进行类型转换:
@Slf4j
@Service
public class VoucherOrderServiceImpl implements VoucherOrderService {@Autowiredprivate RedisIdWorker redisIdWorker;@Autowiredprivate StringRedisTemplate stringRedisTemplate;// 使用静态代码块初始化DefaultRedisScript,添加执行Lua脚本路径private static final DefaultRedisScript<Long> SecondKill_SCRIPT;static {SecondKill_SCRIPT = new DefaultRedisScript<>();SecondKill_SCRIPT.setLocation(new ClassPathResource("Lua/SecondKill.lua"));SecondKill_SCRIPT.setResultType(Long.class);}@Override@Transactionalpublic ResultData VoucherSecondKill(Long VoucherId) {Long UserId = Long.valueOf(CurrentHolder.getCurrent().getId());// 调用脚本int LuaResult = Objects.requireNonNull(stringRedisTemplate.execute(SecondKill_SCRIPT,Collections.emptyList(),VoucherId.toString(),UserId.toString())).intValue();// 错误情况判断if(LuaResult==1) {return ResultData.error("Stock is not Enough!");} else if (LuaResult==2) {return ResultData.error("You have already purchased this voucher!");}// 新增订单记录long VoucherOrderId = redisIdWorker.NewId("Order");return ResultData.success();}
}
执行测试:
分别测试两种错误情况


查看Redis相关,库存减少,优惠券购买用户添加完成,新增下单记录



开启异步队列,封装下单信息
将下单信息封装成为实体类,存入异步队列中,使用BlockingQueue<T>声明阻塞队列,容量为\(1024^2\)(约100w作用)
private BlockingQueue<VoucherOrderData> OrderTasksQueue = new ArrayBlockingQueue<>(1024*1024);@Override
@Transactional
public ResultData VoucherSecondKill(Long VoucherId) {Long UserId = Long.valueOf(CurrentHolder.getCurrent().getId());int LuaResult = Objects.requireNonNull(stringRedisTemplate.execute(SecondKill_SCRIPT,Collections.emptyList(),VoucherId.toString(),UserId.toString())).intValue();if(LuaResult==1) {return ResultData.error("Stock is not Enough!");} else if (LuaResult==2) {return ResultData.error("You have already purchased this voucher!");}long VoucherOrderId = redisIdWorker.NewId("Order");AddNewVoucherOrderToQueue(VoucherId,VoucherOrderId);proxy = (VoucherOrderServiceImpl) AopContext.currentProxy();return ResultData.success("Now OrderId is "+VoucherOrderId);}@Override
@Transactional
public void AddNewVoucherOrderToQueue(Long VoucherId,Long VoucherOrderId) {Long UserId = Long.valueOf(CurrentHolder.getCurrent().getId());VoucherOrderData NewVoucherOrder = new VoucherOrderData();NewVoucherOrder.setId(VoucherOrderId);NewVoucherOrder.setUserId(UserId);NewVoucherOrder.setVoucherId(VoucherId);OrderTasksQueue.add(NewVoucherOrder);
}
开启线程任务,实现异步下单
这一步非常复杂
首先实现异步队列的读取:
使用
Executors.newSingleThreadExecutor()初始化一步处理线程池SECONDKILL_ORDER_EXECUTOR;这是一个单线程线程池,内部只维护一个工作线程,其可以提供独立的执行环境,专门用于从队列中取出订单并执行耗时的数据库操作。
VoucherOrderHandler是内部私有类,实现了Runnable接口,封装了具体的异步消费逻辑,是实际执行数据库写入的操作者。
init()函数添加了@PostConstruct注解,确保在SpringBean初始化完成后立即执行;其作用是将VoucherOrderHandler实例提交给线程池。此时,后台线程启动并进入while(true)循环,开始监听队列。
private static final ExecutorService SECONDKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();@PostConstruct
private void init() {SECONDKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}private class VoucherOrderHandler implements Runnable {@Overridepublic void run() {while(true) {try {VoucherOrderData NewVoucherOrder = OrderTasksQueue.take();AddVoucherOrderFromQueueToDatabase(NewVoucherOrder);} catch (InterruptedException e) {log.error("Calc Order Failed:",e);}}}
}
接下来实现提取出数据后存入数据库
private VoucherOrderService proxy;
@Override
public void AddVoucherOrderFromQueueToDatabase(VoucherOrderData NewVoucherOrder) {// 使用RedissonClient优化锁RLock lock = redissonClient.getLock(REDISSON_KEY_PREFIX+NewVoucherOrder.getUserId());boolean IsLock = lock.tryLock();if(!IsLock) {log.error("Add Database Get Lock Failed!");}try {proxy.CreateNewVoucherOrder(NewVoucherOrder);} finally {lock.unlock();}
}@Override
@Transactional
public void CreateNewVoucherOrder(VoucherOrderData NewVoucherOrder) {Long VoucherId = NewVoucherOrder.getVoucherId();Long UserId = NewVoucherOrder.getUserId();QueryWrapper<VoucherOrderData> orderlwp = new QueryWrapper<VoucherOrderData>();orderlwp.eq("voucher_id",VoucherId).eq("user_id",UserId).select("count(*) as Number");List<Map<String,Object>> Result = voucherOrderMapper.selectMaps(orderlwp);Long Count = (Long) Result.get(0).get("Number");if(Count>0) {log.info("You have already purchased this voucher!");}LambdaUpdateWrapper<VoucherSecondKillData> lwp = new LambdaUpdateWrapper<VoucherSecondKillData>();lwp.eq(VoucherSecondKillData::getVoucherId,VoucherId);voucherSecondKillMapper.UpdateSecondKillVoucher(lwp,1L);voucherOrderMapper.insert(NewVoucherOrder);
}
其实际执行的流程为:
启动阶段:Spring容器初始化
VoucherOrderServiceImpl完成后,执行@PostConstruct,启动SECONDKILL_ORDER_EXECUTOR中的VoucherOrderHandler线程,该线程阻塞在orderTasks.take()处等待。请求阶段:前端发起优惠券秒杀下单请求,在经过处理和校验后最终订单信息存入
OrderTasksQueue队列处理阶段:
VoucherOrderHandler线程从OrderTasksQueue取出订单,获取分布式锁,获取成功则通过代理对象proxy调用CreateVoucherOrder方法执行数据库相关操作
Q:为什么proxy从之前的局部变量变成类成员变量
A:因为AopContext.currentProxy()强依赖于当前线程的ThreadLocal上下文。
使用局部变量声明,在主线程(如Controller调用的Service层方法)中获取proxy并传递给子线程使用,这是可行的,因为传递的是代理对象的引用;但如果试图在子线程内部直接调用AopContext.currentProxy(),通常会抛出IllegalStateException,因为子线程没有继承主线程的 AOP 上下文栈。
使用类成员变量声明,一旦在主线程初始化阶段(如@PostConstruct或首次请求时)将代理对象赋值给成员变量this.proxy,该引用就脱离了ThreadLocal的束缚。在异步线程(如秒杀队列消费者VoucherOrderHandler)中,直接使用this.proxy调用方法是安全且有效的。因为此时只是通过持有引用的普通Java对象调用方法,Spring 的事务拦截器(Interceptor)会根据方法调用触发,并在当前子线程中开启新事务,而不需要访问 AopContext的ThreadLocal。
因此,对于异步秒杀场景,成员变量方式优于在子线程中动态获取,它避免了子线程无法获取AOP上下文的问题。
值得注意的是,在异步秒杀场景中,推荐使用成员变量方式,但应避免使用AopContext.currentProxy()进行初始化,而是采用自我注入的方式,这样更稳健,确实但是抱歉我实在是不想写了
@Service
public class VoucherOrderServiceImpl implements VoucherOrderService {@Autowiredprivate VoucherOrderService self; // Spring 会自动注入代理对象// ... 其他代码@Overridepublic void AddVoucherOrderFromQueueToDatabase(VoucherOrderData NewVoucherOrder) {// ... 锁逻辑// 直接使用注入的代理对象,无需担心 ThreadLocal 上下文问题self.CreateNewVoucherOrder(NewVoucherOrder);// ... 解锁逻辑}
}
基于阻塞队列的异步秒杀存在的问题
①阻塞队列
OrderTasksQueue基于内存实现,存在大小限制
②一旦出现环境异常(例如宕机),基于内存的Redis和阻塞队列数据全部丢失,从而引发数据安全问题
