秒杀场景下,不同步串行执行业务全流程,将下单、扣库存、创建订单、消息通知等非强即时步骤异步化拆解;前端快速返回结果,后端通过Redis和线程池
异步秒杀 异步秒杀是高并发秒杀场景下的核心优化方案,区别于传统同步下单模式,它将库存扣减、订单创建、消息推送等耗时业务进行异步拆解处理,仅把权限校验、流量限流、库存预扣等核心逻辑同步执行并快速响应前端请求。借助消息队列、线程池等异步手段削峰填谷,有效隔离瞬时爆发的海量流量,避免大量并发请求直接冲击数据库,大幅降低数据库锁竞争与连接压力,在提升接口响应速度、优化用户体验的同时,保障秒杀系统的高并发承载能力、稳定性与容错性,从根本上解决秒杀活动中系统卡顿、崩溃、超卖等常见问题。
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)
return 0;
加载lua脚本
//释放秒杀资格判断的lua脚本
private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
//static是静态代码块,只执行一次,用于加载lua脚本
static {
SECKILL_SCRIPT = new DefaultRedisScript<>();
SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
//设置结果类型为Long因为Redis的del指令成功返回1,失败返回0
SECKILL_SCRIPT.setResultType(Long.class);
}
主方法
/**
* 用lua脚本判断秒杀资格
* @param voucherId 优惠券id
* @return 结果
*/
private IVoucherOrderService proxy;
@Override
public Result seckillVoucher(Long voucherId) {
//1执行lua脚本
Long userId = UserHolder.getUser().getId();
//执行脚本
Long result = stringRedisTemplate.execute(
SECKILL_SCRIPT,
//执行 Lua 脚本时,Redis 要求:所有要用到的 Redis key,
// 必须统一放在 KEYS [] 数组里传入,不能在脚本里拼接。
//lua脚本没有用集合传key,所以这里传空集合
Collections.emptyList(),
voucherId.toString(),
userId.toString()
);
//2.判断结果是否为0
int r = result.intValue();
//2.1 不是0,没有购买资格
if(r != 0){
return Result.fail(r==1?"库存不足":"不允许重复下单");
}
//2.2 是0,有购买资格,把下单信息保存到阻塞队列
VoucherOrder voucherOrder = new VoucherOrder();
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
voucherOrder.setUserId(userId);
voucherOrder.setVoucherId(voucherId);
//放到阻塞队列
orderTasks.add(voucherOrder);
//获取代理对象
//因为你要在 异步线程 里调用 带事务 / 带 AOP 注解 的方法,必须用【当前类的代理对象】去调用,
// 直接用 this 调用会让事务 / AOP 失效!
proxy = (IVoucherOrderService) AopContext.currentProxy();
//3.返回结果,订单id
return Result.ok(orderId);
}
阻塞队列
//TODO 阻塞队列--当一个线程尝试从队列中获取元素,没有元素将被阻塞,知道有元素
private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024*1024);
//线程池
private final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
@PostConstruct
/*这是 Spring 提供的注解,标记在方法上,表示该方法会在 Spring 容器初始化完 Bean 之后、
服务启动时自动执行一次*/
//开启后台线程
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("处理订单异常",e);
}
}
}
}
再加一层锁
private void handleVoucherOrder(VoucherOrder voucherOrder) {
//SimpleRedisLock lock = new SimpleRedisLock("lock:"+userId,stringRedisTemplate);
//1.获取用户---从线程池中获得
Long userId = voucherOrder.getUserId();
//2.创建锁对象
RLock lock = redissonClient.getLock("lock:order:"+userId);
//3. 获取锁--互斥效果
boolean isLock = lock.tryLock();
if(!isLock){
log.error("用户{}不允许重复下单",userId);
return;
}
try {
proxy.createVoucherOrder(voucherOrder);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
操作数据库
@Transactional
public void createVoucherOrder(VoucherOrder voucherOrder){
//一人只能下单一次
//用户Id
Long userId = voucherOrder.getUserId();
int count = query().eq("user_id",userId).eq("voucher_id",voucherOrder.getVoucherId()).count();
if(count > 0){
return;
}
//5. 减少库存
boolean success = iSeckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherOrder.getVoucherId())//where voucher_id = ?
//直接判断是否大于0
//解决库存超卖问题
.gt("stock", 0)
.update();
if(!success){
return;
}
save(voucherOrder);
}


1234

被折叠的 条评论
为什么被折叠?



