RabbiMQ消息队列:
RabbitMQ异步执行库存扣减:
What:使用RabbitMQ来取代课程中的队列,将添加订单数据至mysql改成异步的,并且使用令牌桶算法来进行一定程度上的限流。
//令牌桶算法 限流
if (!rateLimiter.tryAcquire(1000, TimeUnit.MILLISECONDS)){
return Result.fail("目前网络正忙,请重试");
}
How:
通过lua脚本去判断库存是否充足以及用户是否多次购买,直接返回报错即可,保证Redis执行的操作的原子性。
若都满足条件,则通过生产者发送VoucherOrder对象到消息队列里面,返回前端订单号即可。
通过消费者去处理消息队列,进行数据库扣减库存、订单生成操作。
基于死信队列处理订单超时未支付:
What:秒杀订单创建后,用户通常有 10-30 分钟支付时间,超时后需自动恢复库存。一般设计到定时的问题都会用到xxl-job等定时任务组件,但是高并发下不推荐定时任务轮询(压力大、延迟高),优先用「消息中间件延迟队列」,这里我们使用RabbitMQ。
Who:
当一个队列中的某条消息满足下列情况之一时,就会成为死信(dead letter):
消费者使用 basic.reject 或 basic.nack 声明消费失败,并且消息的 requeue 参数设置为 false。
过期消息(达到了队列或消息本身设置的过期时间),消息超时后无人消费。
要投递的队列消息堆积满了,最早的消息可能成为死信。

What:
在发送创建订单消息前面先将订单放入延迟队列。
rabbitTemplate.convertAndSend("delay.topic", "delay.order", voucherOrder, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setExpiration("180000");
return message;
}
});
编写lua脚本,实现库存恢复以及去除已购买用户集合,并记录已进行恢复的Order订单号。(防止重复消费)
local VoucherId = ARGV[1]
local UserId = ARGV[2]
local OrderId = ARGV[3]
–变量key
local seckill_key = "seckill:stock:"..VoucherId
local user_key = "seckill:order:"..VoucherId
local order_key = "seckill:order:"..OrderId
if (tonumber(redis.call("sismember",order_key,OrderId))==1) then
return 1
end
–恢复库存
redis.call('incrby',seckill_key,1)
–恢复用户购买资格
redis.call("srem",user_key,UserId)
–将恢复的订单号保存
redis.call("xadd",order_key,OrderId)
return 0
采用乐观锁实现数据库的原子性,以及lua脚本实现Redis的原子性,实现库存恢复。
@Transactional
public void recoverStock(Long orderId, Long voucherId, Long userId) {
// 1. 数据库层面:更新订单状态为“已取消”+恢复库存(乐观锁保证原子性)
// 1.1 先更新订单状态(仅未支付订单可取消)
boolean update = this.lambdaUpdate()
.set(VoucherOrder::getStatus, 4) // 2-已取消
.eq(VoucherOrder::getId, orderId)
.eq(VoucherOrder::getStatus, 1) // 仅未支付订单
.update();
if (!update) {
log.info("订单{}无需恢复(已支付或已取消)", orderId);
return;
}
// 1.2 恢复数据库库存
boolean recoverDb = seckillVoucherMapper.update(voucherId);
if (!recoverDb) {
log.error("订单{}数据库库存恢复失败", orderId);
return;
}
// 2. Redis层面:执行Lua脚本恢复库存
Long redisResult = stringRedisTemplate.execute(
SECKILL_RECOVER_SCRIPT,
Collections.emptyList(),
orderId.toString(),
voucherId.toString(),
userId.toString()
);
if (redisResult == 0) {
log.info("订单{}库存恢复成功(DB+Redis)", orderId);
} else {
log.warn("订单{}Redis库存已恢复,无需重复操作", orderId);
}
}
消费者处理延迟队列步骤:1.先进行订单和支付状态的判断,若已支付或者已取消,直接执行ACK。2.反之,执行库存恢复业务。3.开启消费者确认机制:Auto,实现MessageRecover接口中的RepublishMessageRecover,重试耗尽后,将消息发送到异常交换机和消息队列,进行人工处理。
package com.hmdp.utils;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.service.IVoucherOrderService;
import com.hmdp.service.impl.VoucherOrderServiceImpl;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.Map;
@Component
@Slf4j
public class MQRecoveryReceiver {
@Resource
private IVoucherOrderService voucherOrderService;
// 处理超时未支付订单(核心优化:添加状态校验)
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(name = "dlx.queue", durable = "true"),
exchange = @Exchange(name = "dlx.direct", durable = "true"),
key = "dlx.key"
)
)
public void handleTimeoutOrder(VoucherOrder voucherOrder, com.rabbitmq.client.Channel channel, Message message) throws IOException {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
Long orderId = voucherOrder.getId();
Long voucherId = voucherOrder.getVoucherId();
Long userId = voucherOrder.getUserId();
// 1. 校验订单是否存在
VoucherOrder order = voucherOrderService.getById(orderId);
if (order == null) {
log.info("订单{}不存在,确认消息", orderId);
channel.basicAck(deliveryTag, false);
return;
}
// 2. 校验订单状态:仅未支付订单执行恢复
if (order.getStatus() != 1) {
log.info("订单{}已支付或取消,无需处理超时", orderId);
channel.basicAck(deliveryTag, false);
return;
}
// 3. 执行库存恢复(事务内执行)
voucherOrderService.recoverStock(orderId, voucherId, userId);
// 4. 确认消息
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
log.error("超时订单处理失败, orderId={}", voucherOrder.getId(), e);
// 修正:临时异常时重回队列(限制重试次数,避免死循环)
// 生产环境建议:设置重试次数,超过次数后转发到死信队列归档
channel.basicNack(deliveryTag, false, true);
}
}
}
死信交换机;
package com.hmdp.config;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class RabbitMQConfig {
// 1. 延迟交换机(topic类型,和代码中发送的交换机一致)
@Bean
public Exchange delayTopicExchange() {
return ExchangeBuilder.topicExchange("delay.topic").durable(true).build();
}
// 2. 延迟队列(设置死信参数,不设置队列级别TTL)
@Bean
public Queue delayQueue() {
Map<String, Object> args = new HashMap<>();
args.put("x-dead-letter-exchange", "dlx.direct"); // 死信交换机
args.put("x-dead-letter-routing-key", "dlx.key"); // 死信路由键
// 移除队列级别TTL,仅用消息级别TTL
return QueueBuilder.durable("delay.queue").withArguments(args).build();
}
// 3. 绑定延迟交换机和延迟队列
@Bean
public Binding delayBinding() {
return BindingBuilder.bind(delayQueue()).to(delayTopicExchange()).with("delay.order").noargs();
}
// 4. 死信交换机
@Bean
public Exchange dlxDirectExchange() {
return ExchangeBuilder.directExchange("dlx.direct").durable(true).build();
}
// 5. 死信队列
@Bean
public Queue dlxQueue() {
return QueueBuilder.durable("dlx.queue").build();
}
// 6. 绑定死信交换机和死信队列
@Bean
public Binding dlxBinding() {
return BindingBuilder.bind(dlxQueue()).to(dlxDirectExchange()).with("dlx.key").noargs();
}
@Bean
public Exchange topicExchange() {
return ExchangeBuilder.directExchange("error.exchange").durable(true).build();
}
@Bean
public Queue errorQueue() {
return QueueBuilder.durable("error.queue").build();
}
@Bean
public Binding errorBinding() {
return BindingBuilder.bind(errorQueue()).to(topicExchange()).with("error").noargs();
}
}
支付成功接口,修改支付状态。
用户主动取消订单:先进行订单的判断,判断订单是否存在、是否本人操作以及订单状态(已支付无法取消),进行(3)操作。
// 3. 核心校验:订单存在+归属正确+状态为“未支付”
VoucherOrder order = getById(orderId);
if (order == null) {
return Result.fail("订单不存在");
}
if (!order.getUserId().equals(userId)) {
return Result.fail("无权限取消此订单");
}
if (order.getStatus() != 1) { // 0=未支付,只有未支付订单可取消
return Result.fail("订单已支付/已取消,无需重复操作");
}




