目录
[2.1场景 1:订单超时未支付](#2.1场景 1:订单超时未支付)
[2.1.1配置延迟队列(处理 30 分钟未支付订单)](#2.1.1配置延迟队列(处理 30 分钟未支付订单))
[2.1.3编写库存恢复 Lua 脚本(保证 Redis 原子性)](#2.1.3编写库存恢复 Lua 脚本(保证 Redis 原子性))
[2.1.4实现库存恢复方法(数据库 + Redis 双写)](#2.1.4实现库存恢复方法(数据库 + Redis 双写))
一、对于课程中的队列优化
这里使用RabbitMQ 来取代课程中的队列,将添加订单数据至mysql改成异步的,并且使用++令牌桶++ ++算法++来进行一定程度上的限流。
1.1优化后核心代码
下单代码如下
java
@Resource
private RedisWork redisWork;
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private RedissonClient redissonClient;
private RateLimiter rateLimiter= RateLimiter.create(10);
@Resource
private MQSender mqSender;
private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
private IVoucherOrderService iVoucherOrderService;
static {
SECKILL_SCRIPT = new DefaultRedisScript<>();
SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
SECKILL_SCRIPT.setResultType(Long.class);
}
/**
* 秒杀优惠券
* @param voucherId
* @return
*/
@Override
public Result seckillVoucher(Long voucherId) {
RLock lock = redissonClient.getLock("seckill:lock");
lock.tryLock();
//令牌桶算法 限流
if (!rateLimiter.tryAcquire(1000, TimeUnit.MILLISECONDS)){
return Result.fail("目前网络正忙,请重试");
}
long orderId = redisWork.nextId("order");
Long userId = UserHolder.getUser().getId();
// 1 执行lua脚本
int result = stringRedisTemplate.execute(
SECKILL_SCRIPT,
Collections.emptyList(),
voucherId.toString(), userId.toString(),String.valueOf(orderId)
).intValue();
if (result != 0) {
return Result.fail(result == 1 ? "库存不足" : "不能重复下单");
}
// 2 判断是否为0
// 返回订单id
VoucherOrder voucherOrder = new VoucherOrder();
voucherOrder.setId(orderId);
voucherOrder.setVoucherId(voucherId);
voucherOrder.setUserId(userId);
//发送消息通知向数据库中添加订单数据
mqSender.sendSeckillMessage(JSON.toJSONString(voucherOrder));
return Result.ok(orderId);
}
RabbitMQ的发送者(生产者)
java
package com.hmdp.rabbitmq;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
@Slf4j
@Component
public class MQSender {
@Resource
private RabbitTemplate rabbitTemplate;
/**
* 发送秒杀信息
* @param message
*/
public void sendSeckillMessage(String message){
log.info("发送消息:{}", message);
rabbitTemplate.convertAndSend("seckill.direct", "seckill.success", message);
}
}
RabbitMQ的消费者
java
package com.hmdp.rabbitmq;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import lombok.extern.slf4j.Slf4j;
import com.alibaba.fastjson.JSON;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Component
@Slf4j
public class MQReceiver {
@Resource
IVoucherOrderService voucherOrderService;
@Resource
ISeckillVoucherService seckillVoucherService;
/**
* 监听秒杀消息并下单
* @param message
*/
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(name = "seckill.queue", durable = "true"),
exchange = @Exchange(name = "seckill.direct"),
key = "seckill.success"
))
@Transactional
public void receiveSeckillVoucher(String message){
log.info("收到秒杀消息:{}", message);
VoucherOrder voucherOrder = JSON.parseObject(message, VoucherOrder.class);
Long voucherId = voucherOrder.getVoucherId();
//5.一人一单
Long userId = voucherOrder.getUserId();
//5.1查询订单
int count = voucherOrderService.query().eq("user_id",userId).eq("voucher_id", voucherId).count();
//5.2判断是否存在
if(count>0){
//用户已经购买过了
log.error("该用户已购买过");
return ;
}
log.info("扣减库存");
//6.扣减库存
boolean success = seckillVoucherService
.update()
.setSql("stock = stock-1")
.eq("voucher_id", voucherId)
.gt("stock",0)//cas乐观锁
.update();
if(!success){
log.error("库存不足");
return;
}
//直接保存订单
try {
voucherOrderService.save(voucherOrder);
} catch (Exception e) {
log.error("订单异常信息:{}", e.getMessage());
}
}
}
lua脚本:
java
--1 参数列表
--1.1 优惠卷id
local voucherId = ARGV[1]
--1.2 用户id
local userId = ARGV[2]
--1.3 订单id
local orderId = ARGV[3]
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)
-- 发消息
redis.call('xadd','stream.orders','*','userId',userId,'voucherId',voucherId,'id',orderId)
return 0
二、当前秒杀业务问题
业务比较单薄,说到底只有一个秒杀部分,没有秒杀之后会发生什么,比如秒杀后用户未及时付款导致订单超时,或者用户如果生成订单后取消订单又该如何处理,这些都是需要考虑的,现在我来大概梳理一下这部分补充之后的业务流程
大概可以分为这几步:
秒杀下单、订单创建、支付处理、超时处理、主动退单
这里我们只考虑秒杀下单、订单创建、超时处理、主动退单 这四步,支付处理 这部分可以参照一下黑马商场的那个模块,这里我放张图片快速过一下

秒杀下单和订单创建的逻辑其实和上面基本一致,我们来看一下超时处理 和主动退单
2.1场景 1:订单超时未支付
秒杀订单创建后,用户通常有 10-30 分钟支付时间,超时后需自动恢复库存。一般设计到定时的问题都会用到xxl-job等定时任务组件,但是高并发下不推荐定时任务轮询 (压力大、延迟高),优先用「消息中间件延迟队列」,这里我们使用RabbitMQ。
说到消息队列的延迟,首先想到的肯定是死信队列,这里给出代码:
2.1.1配置延迟队列(处理 30 分钟未支付订单)
新增 RabbitMQ 延迟队列配置,用于监听订单超时:
java
@Configuration
public class SeckillDelayMQConfig {
// 延迟交换机
@Bean
public DirectExchange seckillDelayExchange() {
return ExchangeBuilder.directExchange("seckill.delay.exchange")
.durable(true)
.build();
}
// 延迟队列(30分钟超时)
@Bean
public Queue seckillDelayQueue() {
return QueueBuilder.durable("seckill.delay.queue")
.withArgument("x-message-ttl", 30 * 60 * 1000) // 30分钟TTL
.withArgument("x-dead-letter-exchange", "seckill.dlx.exchange") // 死信交换机
.withArgument("x-dead-letter-routing-key", "seckill.timeout") // 死信路由键
.build();
}
// 绑定延迟交换机和延迟队列
@Bean
public Binding delayBinding() {
return BindingBuilder.bind(seckillDelayQueue())
.to(seckillDelayExchange())
.with("seckill.delay");
}
// 死信交换机(接收超时消息)
@Bean
public DirectExchange seckillDlxExchange() {
return ExchangeBuilder.directExchange("seckill.dlx.exchange")
.durable(true)
.build();
}
// 超时处理队列(最终消费超时订单)
@Bean
public Queue seckillTimeoutQueue() {
return QueueBuilder.durable("seckill.timeout.queue").build();
}
// 绑定死信交换机和超时处理队列
@Bean
public Binding dlxBinding() {
return BindingBuilder.bind(seckillTimeoutQueue())
.to(seckillDlxExchange())
.with("seckill.timeout");
}
}
2.1.2修改秒杀部分的逻辑
在发送创建订单消息前面先将订单放入延迟队列
java
@Override
public Result seckillVoucher(Long voucherId) {
// 现有逻辑:限流、Lua校验、创建订单...(省略)
// 新增:发送延迟消息(30分钟后检查是否支付)
// 消息体包含订单ID、优惠券ID、用户ID
Map<String, Object> delayMsg = new HashMap<>();
delayMsg.put("orderId", orderId);
delayMsg.put("voucherId", voucherId);
delayMsg.put("userId", userId);
mqSender.sendSeckillDelayMessage(delayMsg); // 新增延迟消息发送方法
// 原有:发送创建订单消息
mqSender.sendSeckillMessage(JSON.toJSONString(voucherOrder));
return Result.ok(orderId);
}
// 在MQSender中新增延迟消息发送方法
public void sendSeckillDelayMessage(Map<String, Object> message) {
rabbitTemplate.convertAndSend(
"seckill.delay.exchange",
"seckill.delay",
message
);
}
2.1.3编写库存恢复 Lua 脚本(保证 Redis 原子性)
创建**recover_stock.lua
**,防止并发下重复恢复库存:
java
-- 参数:订单ID、优惠券ID、用户ID
local orderId = ARGV[1]
local voucherId = ARGV[2]
local userId = ARGV[3]
-- Redis键:库存键、用户下单记录键、已恢复订单记录(防重复)
local stockKey = "seckill:stock:" .. voucherId
local orderKey = "seckill:order:" .. voucherId -- 原一人一单的Set集合
local recoveredKey = "seckill:recovered:" .. voucherId -- 记录已恢复的订单
-- 1. 判断订单是否已恢复过(防重复消费)
if redis.call("sismember", recoveredKey, orderId) == 1 then
return 0 -- 已恢复,直接返回
end
-- 2. 恢复库存(stock + 1)
redis.call("incrby", stockKey, 1)
-- 3. 从用户下单记录中移除(允许用户重新秒杀)
redis.call("srem", orderKey, userId)
-- 4. 记录已恢复的订单ID
redis.call("sadd", recoveredKey, orderId)
return 1 -- 恢复成功
2.1.4实现库存恢复方法(数据库 + Redis 双写)
java
@Service
public class VoucherOrderServiceImpl implements IVoucherOrderService {
// 初始化Lua脚本
private static final DefaultRedisScript<Long> RECOVER_SCRIPT;
static {
RECOVER_SCRIPT = new DefaultRedisScript<>();
RECOVER_SCRIPT.setLocation(new ClassPathResource("recover_stock.lua"));
RECOVER_SCRIPT.setResultType(Long.class);
}
/**
* 库存恢复核心方法
* @param orderId 订单ID
* @param voucherId 优惠券ID
* @param userId 用户ID
*/
public void recoverStock(Long orderId, Long voucherId, Long userId) {
// 1. 数据库层面:更新订单状态为"已取消"+恢复库存(乐观锁保证原子性)
// 1.1 先更新订单状态(仅未支付订单可取消)
int updateOrder = lambdaUpdate()
.set(VoucherOrder::getStatus, 2) // 2-已取消
.eq(VoucherOrder::getId, orderId)
.eq(VoucherOrder::getStatus, 0) // 仅未支付订单
.update();
if (updateOrder == 0) {
log.info("订单{}无需恢复(已支付或已取消)", orderId);
return;
}
// 1.2 恢复数据库库存
boolean recoverDb = seckillVoucherService.lambdaUpdate()
.set(SeckillVoucher::getStock, SeckillVoucher::getStock + 1)
.eq(SeckillVoucher::getVoucherId, voucherId)
.update();
if (!recoverDb) {
log.error("订单{}数据库库存恢复失败", orderId);
return;
}
// 2. Redis层面:执行Lua脚本恢复库存
Long redisResult = stringRedisTemplate.execute(
RECOVER_SCRIPT,
Collections.emptyList(),
orderId.toString(),
voucherId.toString(),
userId.toString()
);
if (redisResult == 1) {
log.info("订单{}库存恢复成功(DB+Redis)", orderId);
} else {
log.warn("订单{}Redis库存已恢复,无需重复操作", orderId);
}
}
}
2.1.5新增消费者监听超时队列,调用库存恢复方法:
java
@Component
public class MQRecoveryReceiver {
@Resource
private IVoucherOrderService voucherOrderService;
// 处理超时未支付订单(核心优化:添加状态校验)
@RabbitListener(queues = "seckill.timeout.queue")
public void handleTimeoutOrder(Map<String, Object> msg, Channel channel, Message message) throws IOException {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
Long orderId = Long.parseLong(msg.get("orderId").toString());
Long voucherId = Long.parseLong(msg.get("voucherId").toString());
Long userId = Long.parseLong(msg.get("userId").toString());
// 关键步骤:查询订单当前状态
VoucherOrder order = voucherOrderService.getById(orderId);
if (order == null) {
// 订单不存在,直接确认消息
channel.basicAck(deliveryTag, false);
return;
}
// 若订单已支付(状态1)或已取消(状态2),则不执行恢复
if (order.getStatus() != 0) {
log.info("订单{}已支付或取消,无需处理超时", orderId);
channel.basicAck(deliveryTag, false);
return;
}
// 仅未支付订单(状态0)执行库存恢复
voucherOrderService.recoverStock(orderId, voucherId, userId);
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
log.error("超时订单处理失败", e);
channel.basicNack(deliveryTag, false, false); // 拒绝消息,不重回队列
}
}
}
2.1.6支付成功接口:更新订单状态
在支付成功的业务逻辑中,添加订单状态更新操作(关键是将状态改为 "已支付"):
java
@Service
public class PayService {
@Autowired
private IVoucherOrderService voucherOrderService;
/**
* 用户支付成功后调用
* @param orderId 订单ID
*/
public Result paySuccess(Long orderId) {
// 1. 更新订单状态为"已支付"
boolean success = voucherOrderService.lambdaUpdate()
.set(VoucherOrder::getStatus, 1) // 1-已支付
.eq(VoucherOrder::getId, orderId)
.eq(VoucherOrder::getStatus, 0) // 仅未支付订单可更新
.update();
if (!success) {
return Result.fail("订单状态异常,支付失败");
}
// 2. 其他支付后续逻辑(如生成支付凭证等)
return Result.ok("支付成功");
}
}
2.2场景2:用户主动取消订单
就是直接取消订单,然后恢复库存即可
java
@Service
public class VoucherOrderServiceImpl implements IVoucherOrderService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private SeckillVoucherServiceImpl seckillVoucherService;
@Autowired
private RedissonClient redissonClient;
// 初始化库存恢复Lua脚本(复用之前的)
private static final DefaultRedisScript<Long> RECOVER_STOCK_SCRIPT;
static {
RECOVER_STOCK_SCRIPT = new DefaultRedisScript<>();
RECOVER_STOCK_SCRIPT.setLocation(new ClassPathResource("recover_stock.lua"));
RECOVER_STOCK_SCRIPT.setResultType(Long.class);
}
/**
* 同步取消订单接口(用户主动取消)
* @param orderId 订单ID
* @return 取消结果
*/
@Override
@Transactional // 同步操作,用事务保证DB一致性
public Result cancelOrderSync(Long orderId) {
// 1. 获取当前用户ID(需确保登录态,从UserHolder获取)
Long userId = UserHolder.getUser().getId();
if (userId == null) {
return Result.fail("请先登录");
}
// 2. 分布式锁:防止同一订单被并发取消(避免重复恢复库存)
RLock lock = redissonClient.getLock("lock:cancel:order:" + orderId);
boolean isLock = lock.tryLock(5, 10, TimeUnit.SECONDS); // 5秒等待,10秒自动释放
if (!isLock) {
return Result.fail("取消请求处理中,请稍后再试");
}
try {
// 3. 核心校验:订单存在+归属正确+状态为"未支付"
VoucherOrder order = getById(orderId);
if (order == null) {
return Result.fail("订单不存在");
}
if (!order.getUserId().equals(userId)) {
return Result.fail("无权限取消此订单");
}
if (order.getStatus() != 0) { // 0=未支付,只有未支付订单可取消
return Result.fail("订单已支付/已取消,无需重复操作");
}
// 4. 同步执行库存恢复(DB+Redis)
// 4.1 更新订单状态为"已取消"(事务内执行,失败回滚)
boolean updateOrder = lambdaUpdate()
.set(VoucherOrder::getStatus, 2) // 2=已取消
.eq(VoucherOrder::getId, orderId)
.eq(VoucherOrder::getStatus, 0)
.update();
if (!updateOrder) {
return Result.fail("取消失败,请重试");
}
// 4.2 恢复数据库库存(乐观锁,避免超恢复)
boolean recoverDbStock = seckillVoucherService.lambdaUpdate()
.set(SeckillVoucher::getStock, SeckillVoucher::getStock + 1)
.eq(SeckillVoucher::getVoucherId, order.getVoucherId())
.update();
if (!recoverDbStock) {
throw new RuntimeException("库存恢复失败,取消回滚"); // 抛异常触发事务回滚
}
// 4.3 恢复Redis库存(Lua脚本原子执行)
Long redisResult = stringRedisTemplate.execute(
RECOVER_STOCK_SCRIPT,
Collections.emptyList(),
orderId.toString(),
order.getVoucherId().toString(),
userId.toString()
);
if (redisResult != 1) {
log.warn("订单{}Redis库存恢复重复,已忽略", orderId);
}
return Result.ok("订单取消成功,库存已恢复");
} catch (Exception e) {
log.error("同步取消订单失败", e);
return Result.fail("取消失败,请稍后重试");
} finally {
// 释放分布式锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
三、最后
感兴趣的宝子可以关注一波,后续会更新更多有用的知识!!!
