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

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

改进秒杀业务
需求:
①新增秒杀优惠券的同时,将优惠券信息保存到Redis中
②基于Lua脚本,判断秒杀库存、一人一单,决定用户是否抢购成功
③如果抢购成功,将优惠券id和用户id封装后存入阻塞队列
④开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能
新增秒杀优惠券并保存到Redis
修改新增秒杀优惠券代码,新增添加Redis缓存添加操作
@Service
public class VoucherSecondKillServiceImpl implements VoucherSecondKillService {
@Autowired
private VoucherSecondKillMapper voucherSecondKillMapper;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public 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
-- 库存不足返回1
return 1
end
-- 判断用户是否下单
if(redis.call('sismember',OrderKey,UserId) ==1) then
-- 用户存在说明重复下单,返回2
return 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 {
@Autowired
private RedisIdWorker redisIdWorker;
@Autowired
private 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
@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(),
String.valueOf(UserId)
)).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(),
String.valueOf(UserId)
)).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 {
@Override
public 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 {
@Autowired
private VoucherOrderService self; // Spring 会自动注入代理对象
// ... 其他代码
@Override
public void AddVoucherOrderFromQueueToDatabase(VoucherOrderData NewVoucherOrder) {
// ... 锁逻辑
// 直接使用注入的代理对象,无需担心 ThreadLocal 上下文问题
self.CreateNewVoucherOrder(NewVoucherOrder);
// ... 解锁逻辑
}
}
基于阻塞队列的异步秒杀存在的问题
①阻塞队列
OrderTasksQueue基于内存实现,存在大小限制
②一旦出现环境异常(例如宕机),基于内存的Redis和阻塞队列数据全部丢失,从而引发数据安全问题

浙公网安备 33010602011771号