mobile wallpaper 1
mobile wallpaper 2
mobile wallpaper 3
mobile wallpaper 4
8214 字
22 分钟
RabbitMq高级特性

RabbitMQ 高级特性 - 可靠性与延迟消息#

前言#

在分布式系统中,消息队列扮演着重要角色。但是,如何确保消息的可靠性传递是一个关键问题。

真实业务场景#

考虑一个电商支付场景:

  1. 用户完成支付 → 支付服务记录支付成功
  2. 支付服务通过MQ通知交易服务 → 更新订单状态为已支付
  3. 如果MQ通知失败会怎样?
    • 支付服务:支付成功 ✅
    • 交易服务:订单未支付 ❌
    • 用户体验:明明支付了,为什么显示未支付?😰

核心问题#

  • 如何确保MQ消息的可靠性传递?
  • 消息丢失时有什么兜底方案?
  • 如何处理延迟任务(如订单超时取消)?

让我们一步步解决这些问题。

1. 发送者的可靠性#

目标:确保生产者一定把消息发送到MQ

原则:消息应该至少被消费者处理1次

1.1 消息丢失的可能性分析#

消息从发送者到消费者的传递路径:

🔍 消息丢失的三大环节#

1️⃣ 发送阶段丢失#

  • 网络故障:生产者发送消息时连接MQ失败
  • 交换机不存在:发送到不存在的Exchange
  • 路由失败:Exchange找不到合适的Queue
  • MQ内部异常:消息到达MQ后处理进程异常

2️⃣ MQ存储阶段丢失#

  • 宕机丢失:消息到达MQ,保存到队列后突然宕机
  • 内存丢失:消息只在内存中,未持久化

3️⃣ 消费阶段丢失#

  • 消费者宕机:消息接收后尚未处理就宕机
  • 处理异常:消息处理过程中抛出异常

📋 解决方案概览#

阶段问题解决方案
发送阶段网络故障、路由失败重试机制 + 生产者确认
存储阶段MQ宕机、内存丢失数据持久化 + LazyQueue
消费阶段消费者故障消费者确认 + 失败重试

1.2 生产者重试机制#

🎯 适用场景#

网络不稳定导致与MQ连接中断时,自动重试发送消息。

⚙️ 配置方式#

修改publisher模块的application.yaml:

spring:
rabbitmq:
connection-timeout: 1s # 设置MQ的连接超时时间
template:
retry:
enabled: true # 开启超时重试机制
initial-interval: 1000ms # 失败后的初始等待时间
multiplier: 1 # 失败后下次的等待时长倍数,下次等待时长 = initial-interval * multiplier
max-attempts: 3 # 最大重试次数

🧪 测试效果#

# 停掉RabbitMQ服务
docker stop mq

发送消息后会看到:每隔1秒重试1次,总共重试3次。

⚠️ 注意事项#

SpringAMQP的重试是阻塞式的

  • 重试期间当前线程被阻塞
  • 对性能有要求的业务建议禁用重试
  • 可考虑使用异步线程发送消息

💡 最佳实践#

# 高性能场景配置
spring:
rabbitmq:
template:
retry:
enabled: false # 禁用重试,依赖生产者确认机制

1.3 生产者确认机制#

🎯 解决问题#

即使网络正常,消息也可能在MQ内部丢失:

  • MQ内部处理进程异常
  • Exchange不存在
  • Queue不存在,无法路由

📋 确认机制类型#

RabbitMQ提供了生产者消息确认机制:

确认类型触发时机作用
Publisher Confirm消息到达Exchange确认消息是否成功到达RabbitMQ
Publisher Return消息路由失败确认消息是否成功路由到Queue

🔄 工作流程#

场景分析#

  1. 正常情况:消息到达Exchange且成功路由 → 只收到 Confirm(ack)
  2. Exchange错误:Exchange不存在 → 收到 Confirm(nack)
  3. 路由失败:Exchange存在但Queue不存在 → 收到 Confirm(ack) + Return

1.4 实现生产者确认#

1.4.1 开启生产者确认#

在publisher模块的application.yaml中添加配置:

spring:
rabbitmq:
publisher-confirm-type: correlated # 开启publisher confirm机制,并设置confirm类型
publisher-returns: true # 开启publisher return机制

📋 confirm类型说明#

模式说明使用场景
none关闭confirm机制性能要求极高,容忍少量消息丢失
simple同步阻塞等待MQ回执简单场景,性能要求不高
correlatedMQ异步回调返回回执推荐,性能好且可靠

1.4.2 定义ReturnCallback#

ReturnCallback:全局唯一,统一处理路由失败的消息

创建配置类 com.itheima.publisher.config.MqConfig:

@Slf4j
@AllArgsConstructor
@Configuration
public class MqConfig {
private final RabbitTemplate rabbitTemplate;
@PostConstruct
public void init(){
rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
@Override
public void returnedMessage(ReturnedMessage returned) {
log.error("触发return callback,");
log.debug("exchange: {}", returned.getExchange());
log.debug("routingKey: {}", returned.getRoutingKey());
log.debug("message: {}", returned.getMessage());
log.debug("replyCode: {}", returned.getReplyCode());
log.debug("replyText: {}", returned.getReplyText());
}
});
}
}

1.4.3 定义ConfirmCallback#

ConfirmCallback:每次发送消息时单独定义,处理具体业务逻辑

💡 使用CorrelationData#

CorrelationData核心内容:

  • id:消息的唯一标识,避免回执混淆
  • future:CompletableFuture对象,接收回执结果

📝 代码示例#

@Test
public void testPublisherConfirm(){
CorrelationData correlationData = new CorrelationData();
// 添加回调处理
correlationData.getFuture()
.thenAccept(confirm -> {
if(confirm.isAck()){
log.info("消息发送成功,收到Ack");
}else{
log.info("消息发送失败,原因 {},id {}",confirm.getReason(),correlationData.getId());
}
})
.exceptionally(throwable -> {
log.error("消息发送失败,原因 {}",throwable.getMessage());
return null;
});
rabbitTemplate.convertAndSend("cx.direct", "yell", "hello,confirm", correlationData);
}

🧪 测试结果#

07-16 20:12:50:726 ERROR 11904 --- [nectionFactory2] com.itheima.publisher.config.MqConfig : 触发return callback,
07-16 20:12:50:726 INFO 11904 --- [ 127.0.0.1:5672] c.i.publisher.PublisherApplicationTest : 消息发送成功,收到Ack

📊 结果分析#

场景Confirm回执Return回执说明
正常发送ack无消息成功送达并路由
RoutingKey错误ack触发消息到达Exchange但路由失败
Exchange错误nack无消息未到达Exchange

⚠️ 使用建议#

生产者确认比较消耗MQ性能,一般不建议开启。

何时开启?

  • 对消息可靠性要求极高的业务
  • 大多数情况下只需开启 ConfirmCallback 处理nack

常见失败原因多为编程错误:

  • RoutingKey拼写错误
  • Exchange名称错误
  • 这类问题应该在开发阶段解决

2. MQ的可靠性#

目标:确保MQ不会将消息弄丢

消息到达MQ后,如果MQ不能及时保存,也会导致消息丢失。因此MQ的可靠性同样重要。

2.1 数据持久化#

🎯 问题分析#

默认情况下,RabbitMQ的数据存储在内存中:

  • 优点:读写速度快,性能好
  • 缺点:重启后数据丢失

💾 三级持久化策略#

持久化级别作用重启后效果
交换机持久化保存交换机定义交换机仍存在
队列持久化保存队列定义队列仍存在
消息持久化保存消息内容消息仍存在

2.1.1 交换机持久化#

在控制台的Exchanges页面创建交换机时:

  • Durable:持久化模式(推荐)
  • Transient:临时模式

2.1.2 队列持久化#

在控制台的Queues页面创建队列时:

image-20250717171919967

2.1.3 消息持久化#

发送消息时配置properties:

image-20250716202437751

📝 代码中的持久化配置#

// 声明持久化交换机
@Bean
public DirectExchange durableExchange() {
return ExchangeBuilder.directExchange("durable.exchange")
.durable(true) // 持久化
.build();
}
// 声明持久化队列
@Bean
public Queue durableQueue() {
return QueueBuilder.durable("durable.queue").build(); // 持久化队列
}
// 发送持久化消息
public void sendDurableMessage(String message) {
rabbitTemplate.convertAndSend("durable.exchange", "durable", message, msg -> {
msg.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT); // 消息持久化
return msg;
});
}

⚠️ 重要提醒#

开启持久化机制后:

  1. 生产者确认延迟:MQ会在消息持久化后才发送ACK,确保可靠性
  2. 批量持久化:为减少IO次数,消息每隔~100ms批量持久化
  3. 建议异步:生产者确认全部采用异步方式以提升性能

2.2 LazyQueue(惰性队列)#

🎯 背景问题#

在默认情况下,RabbitMQ将消息保存在内存中以降低延迟。但在特殊情况下会导致消息积压:

📊 消息积压的场景#

  • 消费者故障:宕机或网络故障
  • 流量激增:消息发送量超过消费者处理速度
  • 业务阻塞:消费者处理业务发生阻塞

⚠️ 积压的危害#

  1. 内存占用飙升:消息越积越多
  2. 触发内存预警:达到内存上限
  3. PageOut阻塞:内存消息刷到磁盘,阻塞队列进程
  4. 服务不可用:生产者请求全部被阻塞

💡 LazyQueue解决方案#

从RabbitMQ 3.6.0开始引入惰性队列:

🔄 工作模式对比#

特性普通队列LazyQueue
消息存储优先内存直接磁盘
消费方式内存读取懒加载到内存
内存占用高低
支持容量受内存限制数百万条消息
性能读写快稍慢但稳定

📈 版本演进#

  • 3.12.0之前:需要手动开启Lazy模式
  • 3.12.0及以后:默认行为已类似Lazy模式,更积极地写入磁盘

2.2.1 控制台配置Lazy模式#

创建队列时添加参数:x-queue-mode=lazy

2.2.2 代码配置Lazy模式#

方式一:QueueBuilder方式#

@Bean
public Queue lazyQueue(){
return QueueBuilder
.durable("lazy.queue")
.lazy() // 开启Lazy模式
.build();
}

方式二:注解方式#

@RabbitListener(queuesToDeclare = @Queue(
name = "lazy.queue",
durable = "true",
arguments = @Argument(name = "x-queue-mode", value = "lazy")
))
public void listenLazyQueue(String msg){
log.info("接收到 lazy.queue的消息:{}", msg);
}

2.2.3 更新已有队列为Lazy模式#

命令行方式#

rabbitmqctl set_policy Lazy "^lazy-queue$" '{"queue-mode":"lazy"}' --apply-to queues

命令解析:

  • rabbitmqctl:RabbitMQ命令行工具
  • set_policy:添加策略
  • Lazy:策略名称(自定义)
  • "^lazy-queue$":正则匹配队列名
  • '{"queue-mode":"lazy"}':设置队列模式
  • --apply-to queues:应用到所有队列

控制台方式#

在Admin → Policies页面添加策略:

image-20250717171943508

🎯 使用建议#

适用场景#

  • 消息量大:预期消息积压较多
  • 消费缓慢:消费者处理速度较慢
  • 内存敏感:服务器内存资源有限

性能权衡#

  • 选择LazyQueue:稳定性 > 性能
  • 选择普通队列:性能 > 资源占用

3. 消费者的可靠性#

目标:确保消费者一定要处理消息

当RabbitMQ向消费者投递消息后,需要知道消费者的处理状态。消息投递给消费者并不代表一定被正确消费,可能出现各种故障。

🚨 常见消费故障#

故障类型具体场景后果
网络故障消息投递过程中网络中断消息丢失
消费者宕机接收消息后突然宕机消息丢失
处理异常消息处理逻辑出现bug消息丢失
资源不足内存不足、数据库连接池满消息丢失

❓ 核心问题#

RabbitMQ如何得知消费者的处理状态?

答案就是:消费者确认机制

3.1 消费者确认机制#

🔄 确认机制原理#

消费者处理消息结束后,应该向RabbitMQ发送一个回执,告知消息处理状态。

📋 三种回执类型#

回执类型含义RabbitMQ行为
ack消息处理成功从队列中删除消息 ✅
nack消息处理失败重新投递消息 🔄
reject拒绝消息从队列中删除消息 ❌

💭 使用场景#

  • ack:正常处理完成
  • nack:处理失败,希望重试(如临时网络问题)
  • reject:消息格式错误,无需重试(如消息格式有问题)

⚙️ SpringAMQP的ACK处理模式#

SpringAMQP自动化了消息确认处理,提供三种模式:

📊 模式对比#

模式描述安全性灵活性推荐度
none消息投递后立即ack❌ 很低❌ 无❌ 不推荐
manual手动调用API发送ack/nack✅ 高✅ 最高🔶 特殊场景
auto自动根据异常类型处理✅ 高🔶 中等✅ 推荐

🤖 auto模式的处理逻辑#

SpringAMQP使用AOP对消息处理逻辑做环绕增强:

try {
// 业务逻辑处理
processMessage(message);
return ack; // 成功时自动返回ack
} catch (Exception e) {
if (isBusinessException(e)) {
return nack; // 业务异常,返回nack重试
} else {
return reject; // 消息格式异常,返回reject拒绝
}
}

📋 返回reject的异常类型#

从SpringAMQP 1.3.2开始,默认的ErrorHandler会对以下异常返回reject:

  • MessageConversionException:消息转换异常
  • MethodArgumentNotValidException:参数校验异常
  • MethodArgumentTypeMismatchException:参数类型不匹配
  • NoSuchMethodException:方法不存在
  • ClassCastException:类型转换异常

🛠️ 配置消费者确认模式#

spring:
rabbitmq:
listener:
simple:
acknowledge-mode: auto # 推荐使用auto模式
# acknowledge-mode: none # 不安全,不推荐
# acknowledge-mode: manual # 需要手动编码

🧪 测试不同确认模式#

测试none模式#

@RabbitListener(queues = "simple.queue")
public void listenSimpleQueueMessage(String msg) {
log.info("消费者接收到消息:【{}】", msg);
if (true) {
throw new MessageConversionException("故意的异常");
}
log.info("消息处理完成");
}

配置none模式:

spring:
rabbitmq:
listener:
simple:
acknowledge-mode: none

结果:消息被立即删除,即使出现异常 ❌

测试auto模式#

配置auto模式:

spring:
rabbitmq:
listener:
simple:
acknowledge-mode: auto

测试1:消息转换异常

@RabbitListener(queues = "simple.queue")
public void listenSimpleQueueMessage(String msg) {
log.info("消费者接收到消息:【{}】", msg);
if (true) {
throw new MessageConversionException("消息转换异常"); // 会返回reject
}
}

结果:消息状态先变为unacked,异常后返回reject,消息被删除

测试2:业务异常

@RabbitListener(queues = "simple.queue")
public void listenSimpleQueueMessage(String msg) {
log.info("消费者接收到消息:【{}】", msg);
if (true) {
throw new RuntimeException("业务异常"); // 会返回nack
}
}

结果:消息状态先变为unacked,异常后返回nack,消息重新回到队列

image-20250717171958197

3.2 失败重试机制#

🎯 问题场景#

当消费者出现异常后,消息会不断requeue(重新入队)到队列头部,并重新发送给消费者。

⚠️ 无限循环的危害#

如果消费者一直无法执行成功:

  1. 消息无限循环:requeue → 投递 → 异常 → requeue → …
  2. MQ压力飙升:消息处理频率激增
  3. 资源浪费:CPU、网络、磁盘IO持续消耗
  4. 影响其他消息:阻塞正常消息处理

💡 本地重试机制#

Spring提供了消费者失败重试机制:

  • 本地重试:在消费者端进行重试,而不是requeue到MQ
  • 重试限制:设置最大重试次数,避免无限循环
  • 优雅降级:重试耗尽后的失败处理策略

⚙️ 配置本地重试#

修改consumer服务的application.yml:

spring:
rabbitmq:
listener:
simple:
retry:
enabled: true # 开启消费者失败重试
initial-interval: 1000ms # 初始失败等待时长为1秒
multiplier: 1 # 失败等待时长倍数,下次等待时长 = multiplier * last-interval
max-attempts: 3 # 最大重试次数
stateless: true # true无状态;false有状态,包含事务上下文

📊 参数说明#

参数说明推荐值
enabled是否开启重试true
initial-interval初始重试间隔1000ms
multiplier重试间隔倍数1(固定间隔)或2(指数退避)
max-attempts最大重试次数3-5
stateless是否有状态true(大多数场景)

🔄 stateless参数详解#

模式特点适用场景
stateless: true每次重试都是独立的无事务、幂等操作
stateless: false保留上次重试的上下文信息包含事务的业务

🧪 测试重试效果#

@RabbitListener(queues = "simple.queue")
public void listenSimpleQueueMessage(String msg) {
log.info("消费者接收到消息:【{}】", msg);
throw new RuntimeException("模拟业务异常");
}

测试结果:

  1. 消费者本地重试3次(1秒间隔)
  2. 重试3次后抛出AmqpRejectAndDontRequeueException异常
  3. Spring返回reject,消息被删除

📊 重试机制对比#

重试方式重试位置MQ压力控制精度推荐度
requeue重试MQ队列❌ 高❌ 低❌ 不推荐
本地重试消费者本地✅ 低✅ 高✅ 推荐

🔄 重试流程图#

graph TD A[接收消息] --> B[处理消息] B --> C{处理成功?} C -->|是| D[返回ACK] C -->|否| E{重试次数<最大值?} E -->|是| F[等待间隔时间] F --> G[本地重试] G --> B E -->|否| H[返回REJECT] H --> I[消息被删除]

⚠️ 注意事项#

本地重试的行为:

  • ✅ 消息不会requeue到队列
  • ✅ 减少MQ压力
  • ✅ 更精确的重试控制
  • ❌ 重试耗尽后消息被丢弃(需要失败处理策略)

3.3 失败处理策略#

🎯 问题分析#

本地重试达到最大次数后,消息会被丢弃。这在对消息可靠性要求较高的业务场景下显然不合适。

💡 解决方案#

Spring允许自定义重试次数耗尽后的消息处理策略,通过MessageRecovery接口定义。

📋 三种处理策略#

策略类型类名行为适用场景
直接丢弃RejectAndDontRequeueRecoverer直接reject,丢弃消息对消息丢失容忍度高
重新入队ImmediateRequeueMessageRecoverer返回nack,消息重新入队临时故障,稍后可能恢复
转发异常队列RepublishMessageRecoverer转发到指定交换机推荐,便于人工处理

🏆 推荐方案:RepublishMessageRecoverer#

将失败消息投递到专门的异常队列,后续由人工集中处理。

🔧 实现步骤#

1)定义异常消息的交换机和队列

在consumer服务中创建配置:

package com.itheima.consumer.config;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.retry.MessageRecoverer;
import org.springframework.amqp.rabbit.retry.RepublishMessageRecoverer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConditionalOnProperty(name = "spring.rabbitmq.listener.simple.retry.enabled", havingValue = "true")
public class ErrorMessageConfig {
/**
* 定义异常交换机
*/
@Bean
public DirectExchange errorMessageExchange(){
return new DirectExchange("error.direct");
}
/**
* 定义异常队列
*/
@Bean
public Queue errorQueue(){
return new Queue("error.queue", true);
}
/**
* 绑定异常队列到异常交换机
*/
@Bean
public Binding errorBinding(Queue errorQueue, DirectExchange errorMessageExchange){
return BindingBuilder.bind(errorQueue).to(errorMessageExchange).with("error");
}
/**
* 定义RepublishMessageRecoverer,关联队列和交换机
*/
@Bean
public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){
return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
}
}

🎯 配置说明#

  • @ConditionalOnProperty:只有开启重试机制时才生效
  • error.direct:异常交换机名称
  • error.queue:异常队列名称
  • error:路由键
  • RepublishMessageRecoverer:失败消息重新发布处理器

🧪 测试效果#

1)重启consumer服务

2)发送消息触发异常

@RabbitListener(queues = "simple.queue")
public void listenSimpleQueueMessage(String msg) {
log.info("消费者接收到消息:【{}】", msg);
throw new RuntimeException("模拟业务异常");
}

3)观察结果

  • 本地重试3次
  • 重试耗尽后消息被发送到error.queue
  • 原始队列消息被删除
  • 异常队列可以看到失败的消息

🔄 处理流程图#

graph TD A[消息处理] --> B{成功?} B -->|是| C[返回ACK] B -->|否| D{重试次数<最大值?} D -->|是| E[本地重试] E --> A D -->|否| F[RepublishMessageRecoverer] F --> G[发送到error.direct交换机] G --> H[路由到error.queue] H --> I[人工处理]

🛠️ 异常消息的处理#

方式一:人工处理#

  • 登录RabbitMQ控制台
  • 查看error.queue中的消息
  • 分析失败原因
  • 修复问题后重新发送

方式二:监听异常队列#

@RabbitListener(queues = "error.queue")
public void handleErrorMessage(String msg,
@Header Map<String, Object> headers,
@Header("x-exception-message") String exceptionMessage) {
log.error("收到异常消息:{}", msg);
log.error("异常信息:{}", exceptionMessage);
log.error("原始headers:{}", headers);
// 1. 记录到数据库
// 2. 发送告警通知
// 3. 尝试修复后重新发送
}

📊 各种策略对比#

策略消息丢失风险处理复杂度人工干预推荐指数
RejectAndDontRequeue❌ 高✅ 简单❌ 无法⭐⭐
ImmediateRequeue✅ 低❌ 复杂❌ 困难⭐⭐
RepublishMessageRecoverer✅ 低🔶 中等✅ 便于⭐⭐⭐⭐⭐

💡 最佳实践#

  1. 开发环境:使用RejectAndDontRequeue快速调试
  2. 测试环境:使用RepublishMessageRecoverer测试异常处理
  3. 生产环境:使用RepublishMessageRecoverer + 监控告警

3.4 业务幂等性#

🎯 什么是幂等性?#

幂等是数学概念,用函数表达:f(x) = f(f(x)),例如求绝对值函数。

在程序开发中:同一个业务,执行一次或多次对业务状态的影响是一致的。

📊 幂等 vs 非幂等操作#

操作类型示例重复执行结果是否幂等
查询操作根据ID查询用户信息结果不变✅ 幂等
删除操作根据ID删除数据数据已删除✅ 幂等
新增操作插入唯一约束数据插入失败✅ 幂等
更新操作设置状态为已支付状态不变✅ 幂等
累加操作库存数量+1重复累加❌ 非幂等
扣减操作退款恢复库存重复恢复❌ 非幂等

🚨 重复执行的业务风险#

💰 电商场景示例#

  1. 订单取消:恢复库存业务如果重复执行 → 库存重复增加
  2. 退款业务:重复退款 → 商家经济损失
  3. 积分发放:重复发放 → 用户获得额外积分

🔄 消息重复投递的场景#

在实际业务中,消息重复执行很常见:

📋 常见场景#

场景原因后果
页面重复提交用户点击卡顿后多次点击重复下单
服务重试网络超时后重试调用重复处理
MQ重复投递消费者确认失败后重投重复消费

🎭 具体案例:支付状态更新#

sequenceDiagram participant U as 用户 participant P as 支付服务 participant MQ as RabbitMQ participant T as 交易服务 U->>P: 支付成功 P->>MQ: 发送支付成功消息 MQ->>T: 投递消息 T->>T: 更新订单状态:未支付→已支付 ✅ Note over MQ,T: 网络故障,确认失败 MQ->>T: 重新投递消息 Note over U,T: 此时用户已退款,订单状态:已退款 T->>T: 再次更新:已退款→已支付 ❌

问题:用户已退款,但订单状态被重复消息改为已支付!

💡 幂等性解决方案#

方案一:唯一消息ID#

思路:为每条消息生成唯一ID,处理成功后记录ID,重复消息根据ID判断跳过

1)开启MessageID功能

@Bean
public MessageConverter messageConverter(){
Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
// 配置自动创建消息ID,用于识别不同消息
converter.setCreateMessageIds(true);
return converter;
}

2)消费者端处理

@RabbitListener(queues = "payment.queue")
public void handlePaymentMessage(PaymentMessage msg,
@Header("spring_returned_message_correlation") String messageId) {
// 1. 检查消息ID是否已处理
if (messageIdService.isProcessed(messageId)) {
log.info("消息已处理,跳过:{}", messageId);
return;
}
// 2. 处理业务逻辑
processPayment(msg);
// 3. 记录消息ID
messageIdService.markProcessed(messageId);
}

3)MessageID服务实现

@Service
public class MessageIdService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String PROCESSED_KEY_PREFIX = "msg:processed:";
private static final long EXPIRE_TIME = 24 * 60 * 60; // 24小时过期
public boolean isProcessed(String messageId) {
String key = PROCESSED_KEY_PREFIX + messageId;
return redisTemplate.hasKey(key);
}
public void markProcessed(String messageId) {
String key = PROCESSED_KEY_PREFIX + messageId;
redisTemplate.opsForValue().set(key, "1", EXPIRE_TIME, TimeUnit.SECONDS);
}
}

方案二:业务状态判断(推荐)#

思路:基于业务逻辑本身判断是否重复,无需额外存储

示例:支付状态更新

@Override
public void markOrderPaySuccess(Long orderId) {
// 方式1:先查询再更新(存在并发问题)
Order old = getById(orderId);
if (old == null || old.getStatus() != 1) {
// 订单不存在或状态不是未支付,跳过处理
return;
}
// 更新订单状态
Order order = new Order();
order.setId(orderId);
order.setStatus(2); // 已支付
order.setPayTime(LocalDateTime.now());
updateById(order);
}

优化:原子操作(推荐)

@Override
public void markOrderPaySuccess(Long orderId) {
// 使用WHERE条件确保幂等性
int updated = lambdaUpdate()
.set(Order::getStatus, 2) // 设置为已支付
.set(Order::getPayTime, LocalDateTime.now())
.eq(Order::getId, orderId) // 订单ID匹配
.eq(Order::getStatus, 1) // 只有未支付状态才更新
.update();
if (updated == 0) {
log.info("订单{}状态更新跳过,可能已处理", orderId);
}
}

对应SQL:

UPDATE `order`
SET status = 2, pay_time = NOW()
WHERE id = ? AND status = 1

📊 两种方案对比#

方案优点缺点适用场景
唯一消息ID通用性强,适用各种业务需要额外存储,增加复杂度业务逻辑复杂,难以判断重复
业务状态判断无需额外存储,性能好需要业务支持状态判断推荐,业务有明确状态流转

🏆 最佳实践#

  1. 优先使用业务状态判断:利用数据库约束和WHERE条件
  2. 设计合理的状态流转:确保状态变更的原子性
  3. 必要时使用消息ID:复杂业务场景的补充方案
  4. 幂等性测试:模拟重复消息验证幂等效果

3.5 兜底方案#

🎯 为什么需要兜底方案?#

虽然通过各种机制提升了消息可靠性,但仍无法保证100%可靠。

万一MQ通知失败怎么办? 需要其他兜底方案确保订单支付状态一致性。

💡 主动查询兜底#

核心思想:既然MQ通知可能失败,那么交易服务就主动查询支付状态。

🔄 完整流程图#

image-20250717172024959

黄色圈起来的部分就是MQ通知失败后的兜底处理方案。

⏰ 定时查询策略#

🤔 何时查询?#

用户支付时间不确定,如果查询时机不正确可能查到错误状态。

💡 解决方案:定时任务#

采用定时任务定期查询支付状态:

  • 查询频率:每隔20秒查询一次
  • 查询逻辑:判断支付状态,如已支付则更新订单状态
  • 查询时长:根据业务设定(如30分钟内)

📋 实现示例#

1)定时任务配置

@Component
@Slf4j
public class OrderStatusCheckTask {
@Autowired
private OrderService orderService;
@Autowired
private PaymentService paymentService;
/**
* 每20秒检查一次未支付订单
*/
@Scheduled(fixedDelay = 20000)
public void checkUnpaidOrders() {
// 查询5分钟内创建但未支付的订单
LocalDateTime cutoffTime = LocalDateTime.now().minusMinutes(5);
List<Order> unpaidOrders = orderService.getUnpaidOrdersAfter(cutoffTime);
for (Order order : unpaidOrders) {
checkAndUpdateOrderStatus(order);
}
}
private void checkAndUpdateOrderStatus(Order order) {
try {
// 查询支付状态
PaymentStatus status = paymentService.getPaymentStatus(order.getId());
if (status == PaymentStatus.PAID) {
// 支付成功,更新订单状态
orderService.markOrderPaySuccess(order.getId());
log.info("定时任务检测到订单{}已支付,状态已更新", order.getId());
}
} catch (Exception e) {
log.error("检查订单{}支付状态失败", order.getId(), e);
}
}
}

2)查询未支付订单

@Service
public class OrderServiceImpl implements OrderService {
/**
* 查询指定时间后创建的未支付订单
*/
public List<Order> getUnpaidOrdersAfter(LocalDateTime createTime) {
return lambdaQuery()
.eq(Order::getStatus, 1) // 未支付状态
.ge(Order::getCreateTime, createTime) // 创建时间大于指定时间
.list();
}
}

🔄 双重保障机制#

📊 保障策略对比#

保障方式实时性可靠性实现复杂度资源消耗
MQ通知✅ 高🔶 中等🔶 中等✅ 低
定时查询❌ 低✅ 高✅ 简单❌ 高
双重保障✅ 高✅ 高❌ 复杂🔶 中等

🏆 最佳实践组合#

graph TD A[用户支付成功] --> B(支付服务记录支付) B --> C(发送MQ消息) C --> D{MQ通知成功?} D -- "是" --> E(交易服务更新订单状态) D -- "否" --> F(定时任务兜底) F --> G(主动查询支付状态) G --> H{已支付?} H -- "是" --> E H -- "否" --> I(继续等待下次检查) E --> J(订单状态一致)

⚙️ 定时任务优化#

📈 性能优化策略#

1)减少查询范围

// 只查询特定时间窗口内的订单
LocalDateTime startTime = LocalDateTime.now().minusMinutes(30); // 30分钟前
LocalDateTime endTime = LocalDateTime.now().minusMinutes(1); // 1分钟前
List<Order> orders = orderService.getUnpaidOrdersBetween(startTime, endTime);

2)分批处理

@Scheduled(fixedDelay = 20000)
public void checkUnpaidOrdersBatch() {
int pageSize = 100;
int page = 0;
while (true) {
List<Order> orders = orderService.getUnpaidOrdersPaged(page, pageSize);
if (orders.isEmpty()) break;
for (Order order : orders) {
checkAndUpdateOrderStatus(order);
}
page++;
}
}

3)异步处理

@Async
public void checkOrderStatusAsync(Order order) {
checkAndUpdateOrderStatus(order);
}

📊 完整可靠性方案总结#

环节主要策略兜底方案
发送阶段重试机制 + 生产者确认业务补偿
存储阶段数据持久化 + LazyQueue集群部署
消费阶段消费者确认 + 失败重试异常队列
业务幂等状态判断 + 唯一ID-
最终一致性MQ通知定时任务主动查询

🎯 方案选择建议#

📊 业务场景匹配#

业务类型推荐方案理由
高实时性MQ通知为主用户体验优先
高可靠性MQ + 定时任务数据一致性优先
资源敏感延长MQ重试减少定时任务开销
复杂业务全套方案多重保障

这样的设计既保证了实时性,又提供了可靠的兜底机制,确保了订单支付状态的最终一致性。

4. 延迟消息#

4.1 延迟消息的应用场景#

📱 电商支付超时场景#

在电商系统中,为了更好的用户体验,用户下单后通常立即扣减库存:

优点:

  • ✅ 座位/商品立即锁定
  • ✅ 用户体验好
  • ✅ 防止超卖

问题:

  • ❌ 用户下单后一直不付款
  • ❌ 库存长期被占用
  • ❌ 其他用户无法购买
  • ❌ 商户利益受损

💡 解决方案:延迟任务#

业务需求:对于超过一定时间未支付的订单,应该立即取消并释放库存。

🎯 典型场景#

业务场景延迟时间触发动作
订单支付30分钟取消订单,释放库存
优惠券3天回收未使用优惠券
账户激活24小时发送激活提醒邮件
会员续费7天发送续费提醒

❓ 技术挑战#

如何准确实现”下单后第30分钟检查支付状态”?

这就是延迟任务,要实现延迟任务,最简单的方案就是利用MQ的延迟消息。

🔧 RabbitMQ延迟消息方案#

方案原理优点缺点推荐度
死信交换机+TTL消息过期变死信不需插件时间不准确⭐⭐⭐
延迟消息插件插件原生支持时间准确需要插件⭐⭐⭐⭐⭐

4.2 死信交换机和延迟消息#

4.2.1 死信交换机#

🎯 什么是死信?#

当队列中的消息满足以下条件之一时,可以成为死信(dead letter):

死信产生条件描述示例场景
消费者拒绝basic.reject或basic.nack且requeue=false消息格式错误
消息过期超过TTL时间无人消费延迟消息场景
队列满载队列达到最大长度,无法投递消息积压

🔄 死信交换机工作原理#

当队列通过dead-letter-exchange属性指定了交换机,死信就会投递到该交换机:

// 配置死信交换机
@Bean
public Queue orderQueue() {
return QueueBuilder.durable("order.queue")
.withArgument("x-dead-letter-exchange", "order.dlx.exchange") // 死信交换机
.withArgument("x-dead-letter-routing-key", "order.timeout") // 死信路由键
.withArgument("x-message-ttl", 30 * 60 * 1000) // 30分钟TTL
.build();
}

📋 死信交换机的作用#

  1. 异常处理:收集处理失败的消息
  2. 过载保护:收集队列满载时的消息
  3. 延迟消息:⭐ 收集TTL过期的消息(延迟消息实现)

4.2.2 延迟消息实现#

🏗️ 系统架构设计#

假设我们有这样的绑定关系:

🔄 工作流程#

1)发送延迟消息

// 发送消息到ttl.fanout,routingKey为blue,TTL=5000ms
rabbitTemplate.convertAndSend("ttl.fanout", "blue", "延迟消息", message -> {
message.getMessageProperties().setExpiration("5000"); // 5秒后过期
return message;
});

2)消息流转过程

graph LR A[发送消息] --> B[ttl.fanout交换机] B --> C[ttl.queue队列] C --> D[等待5秒TTL过期] D --> E[变成死信] E --> F[投递到hmall.direct] F --> G[路由到direct.queue1] G --> H[消费者消费]

3)时间轴分析

时间点事件消息位置
T+0s发送消息ttl.queue
T+1-4s等待过期ttl.queue
T+5sTTL过期变成死信
T+5s死信投递direct.queue1
T+5s消费者消费被处理

🎯 实现效果#

成功实现延迟消息:发送者发送消息,消费者在5秒后收到消息!

4.2.3 死信+TTL方案总结#

✅ 优点#

  • 无需额外插件
  • 利用RabbitMQ原生特性
  • 配置相对简单

❌ 缺点#

⚠️ 时间不准确问题

RabbitMQ的消息过期基于追溯方式实现:

  • 消息TTL到期后不会立即处理
  • 只有当消息处于队列头部时才会被处理
  • 队列消息堆积时,过期消息可能不会按时处理

📊 场景分析#

队列状态延迟准确性适用性
空闲状态✅ 准确✅ 适用
少量积压🔶 基本准确🔶 可接受
大量积压❌ 不准确❌ 不适用

💡 使用建议#

  • 适用场景:对时间精度要求不高的业务
  • 不适用场景:需要精确延迟时间的业务
  • 替代方案:延迟消息插件

4.3 DelayExchange插件(推荐方案)#

🎯 插件优势#

基于死信队列虽然可以实现延迟消息,但过于复杂。RabbitMQ社区提供了延迟消息插件来实现相同效果。

📊 方案对比#

方案特点死信+TTLDelayExchange插件
配置复杂度❌ 复杂✅ 简单
时间准确性❌ 不准确✅ 准确
性能开销✅ 低🔶 中等
插件依赖✅ 无需插件❌ 需要插件
推荐度⭐⭐⭐⭐⭐⭐⭐⭐

🔄 延迟交换机工作机制#

🆚 三种流程对比#

普通交换机流程:

发送者 → 交换机 → [立即路由] → 队列 → 消费者

TTL过期流程:

发送者 → 交换机 → [立即路由] → 队列 → [等待TTL过期] → 死信交换机 → 死信队列 → 消费者

延迟交换机流程:

发送者 → 延迟交换机 → [等待延迟时间] → 路由到队列 → 消费者 [立即消费]

🎯 核心区别#

阶段死信+TTL延迟交换机
消息存储队列中等待过期交换机中暂存
延迟控制队列TTL设置消息头x-delay
路由时机立即路由,队列等待延迟后才路由

4.3.1 插件下载与安装#

📥 下载插件#

官方地址:GitHub - rabbitmq/rabbitmq-delayed-message-exchange

选择版本:下载与RabbitMQ版本对应的.ez文件

⚙️ 安装步骤#

1)复制插件到plugins目录

# 复制到RabbitMQ的plugins目录
cp rabbitmq_delayed_message_exchange-3.10.2.ez /usr/lib/rabbitmq/lib/rabbitmq_server-3.10.2/plugins/

2)启用插件

# 进入sbin目录
cd /usr/lib/rabbitmq/lib/rabbitmq_server-3.10.2/sbin
# 启用插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange

Windows示例:

Terminal window
C:\New_SoftWare\rabbitmq_server-4.1.0\sbin>rabbitmq-plugins enable rabbitmq_delayed_message_exchange

3)重启RabbitMQ服务

systemctl restart rabbitmq-server

4.3.2 声明延迟交换机#

🎯 重要原则#

延迟交换机只需在消费者端声明,生产者直接使用交换机名称发送消息即可。

📝 方式一:基于注解#

@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "delay.queue", durable = "true"),
exchange = @Exchange(name = "delay.direct", delayed = "true"), // 关键:delayed = "true"
key = "delay"
))
public void listenDelayMessage(String msg){
log.info("接收到delay.queue的延迟消息:{}", msg);
}

📝 方式二:基于@Bean配置#

@Configuration
public class DelayExchangeConfig {
/**
* 延迟交换机 - 本质是DirectExchange + 延迟功能
*/
@Bean
public DirectExchange delayExchange() {
return ExchangeBuilder.directExchange("delay.exchange")
.durable(true)
.delayed() // 🎯 关键:启用延迟功能
.build();
}
/**
* 普通队列
*/
@Bean
public Queue delayQueue() {
return new Queue("delay.queue", true);
}
/**
* 绑定队列和延迟交换机
*/
@Bean
public Binding delayBinding(Queue delayQueue, DirectExchange delayExchange){
return BindingBuilder.bind(delayQueue)
.to(delayExchange)
.with("delay"); // routing key
}
}

4.3.3 发送延迟消息#

🔑 核心要点#

发送消息时,必须通过x-delay属性设定延迟时间:

📝 基础用法#

/**
* 测试延迟消息
*/
@Test
public void testDelayMessage() {
String exchangeName = "delay.exchange";
String routingKey = "delay";
String message = "hello,delay";
// 使用MessagePostProcessor设置延迟时间
rabbitTemplate.convertAndSend(exchangeName, routingKey, message, msg -> {
// 🎯 设置延迟时间为10秒(毫秒单位)
msg.getMessageProperties().setHeader("x-delay", 10000);
return msg;
});
log.info("延迟消息已发送,将在10秒后被消费");
}

🔧 MessagePostProcessor说明#

MessagePostProcessor是函数式接口,用于在发送前修改消息属性:

📋 实用示例#

1)封装延迟消息工具方法

@Service
public class DelayMessageService {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 发送延迟消息
* @param exchange 交换机名称
* @param routingKey 路由键
* @param message 消息内容
* @param delaySeconds 延迟秒数
*/
public void sendDelayMessage(String exchange, String routingKey, Object message, int delaySeconds) {
rabbitTemplate.convertAndSend(exchange, routingKey, message, msg -> {
msg.getMessageProperties().setHeader("x-delay", delaySeconds * 1000);
return msg;
});
}
}

2)业务场景应用

@Service
public class OrderService {
@Autowired
private DelayMessageService delayMessageService;
/**
* 创建订单并发送超时检查消息
*/
public void createOrder(Order order) {
// 1. 保存订单
orderMapper.insert(order);
// 2. 发送30分钟后的超时检查消息
delayMessageService.sendDelayMessage(
"order.delay.exchange",
"order.timeout.check",
order.getId(),
30 * 60 // 30分钟
);
log.info("订单创建成功,30分钟后将检查支付状态");
}
}

3)多层延迟检查

/**
* 分层检查订单状态
*/
public void sendOrderCheckMessages(Long orderId) {
// 5分钟后第一次检查
delayMessageService.sendDelayMessage("order.delay.exchange", "order.check", orderId, 5 * 60);
// 15分钟后第二次检查
delayMessageService.sendDelayMessage("order.delay.exchange", "order.check", orderId, 15 * 60);
// 30分钟后最终检查
delayMessageService.sendDelayMessage("order.delay.exchange", "order.timeout", orderId, 30 * 60);
}

⚠️ 重要注意事项#

🚨 性能警告#

延迟消息插件内部机制:

  • 本地数据库表:维护延迟消息
  • Elang Timers:实现计时功能
  • 内存开销:长延迟消息占用CPU资源
  • 时间误差:极长延迟可能存在误差

💡 使用建议#

延迟时长建议原因
< 1小时✅ 推荐使用性能影响小
1-6小时🔶 谨慎使用注意监控资源
> 6小时❌ 不建议考虑其他方案

🔄 长延迟替代方案#

方案1:分段延迟

// 不要发送6小时的延迟消息
// 而是每小时发送一次检查消息
for (int i = 1; i <= 6; i++) {
sendDelayMessage("check.exchange", "hourly.check", orderId, i * 60 * 60);
}

方案2:定时任务配合

// 延迟消息 + 定时任务的混合方案
// 延迟消息处理短期任务(< 1小时)
// 定时任务处理长期任务(> 1小时)

🎯 延迟消息最佳实践#

  1. 优先使用延迟消息插件:准确性高于死信+TTL
  2. 控制延迟时长:避免超长延迟消息
  3. 合理设计粒度:分段检查代替单次长延迟
  4. 监控资源使用:关注CPU和内存占用
  5. 幂等性设计:确保重复消息处理的安全性

4.4 延迟消息实际应用场景#

📱 常见业务场景#

1️⃣ 电商订单管理#

@Service
public class OrderDelayService {
@Autowired
private DelayMessageService delayMessageService;
/**
* 订单超时取消场景
*/
public void handleOrderTimeout(Long orderId) {
// 发送分段检查消息,避免长时间占用资源
// 10分钟后首次检查
delayMessageService.sendDelayMessage(
"order.delay.exchange",
"order.check",
orderId,
10 * 60
);
// 20分钟后二次检查
delayMessageService.sendDelayMessage(
"order.delay.exchange",
"order.check",
orderId,
20 * 60
);
// 30分钟后最终超时处理
delayMessageService.sendDelayMessage(
"order.delay.exchange",
"order.timeout",
orderId,
30 * 60
);
}
/**
* 处理订单检查消息
*/
@RabbitListener(queues = "order.check.queue")
public void handleOrderCheck(Long orderId) {
Order order = orderService.getById(orderId);
if (order == null || order.getStatus() != OrderStatus.UNPAID) {
log.info("订单{}状态已变更,跳过检查", orderId);
return;
}
// 检查支付状态
PaymentStatus paymentStatus = paymentService.getPaymentStatus(orderId);
if (paymentStatus == PaymentStatus.PAID) {
orderService.markOrderPaid(orderId);
log.info("订单{}支付成功", orderId);
}
}
/**
* 处理订单超时
*/
@RabbitListener(queues = "order.timeout.queue")
public void handleOrderTimeout(Long orderId) {
Order order = orderService.getById(orderId);
if (order != null && order.getStatus() == OrderStatus.UNPAID) {
// 取消订单并释放库存
orderService.cancelOrder(orderId);
inventoryService.releaseInventory(orderId);
log.info("订单{}超时取消", orderId);
}
}
}

2️⃣ 优惠券过期提醒#

@Service
public class CouponReminderService {
/**
* 优惠券到期提醒
*/
public void sendCouponExpiryReminder(Long couponId, LocalDateTime expiryTime) {
LocalDateTime now = LocalDateTime.now();
// 过期前3天提醒
LocalDateTime remind3Days = expiryTime.minusDays(3);
if (remind3Days.isAfter(now)) {
long delay3Days = Duration.between(now, remind3Days).getSeconds();
delayMessageService.sendDelayMessage(
"coupon.reminder.exchange",
"coupon.expire.3days",
couponId,
(int) delay3Days
);
}
// 过期前1天提醒
LocalDateTime remind1Day = expiryTime.minusDays(1);
if (remind1Day.isAfter(now)) {
long delay1Day = Duration.between(now, remind1Day).getSeconds();
delayMessageService.sendDelayMessage(
"coupon.reminder.exchange",
"coupon.expire.1day",
couponId,
(int) delay1Day
);
}
}
@RabbitListener(queues = "coupon.expire.3days.queue")
public void handleCoupon3DaysReminder(Long couponId) {
// 发送3天到期提醒
notificationService.sendCouponExpiryReminder(couponId, 3);
}
@RabbitListener(queues = "coupon.expire.1day.queue")
public void handleCoupon1DayReminder(Long couponId) {
// 发送1天到期提醒
notificationService.sendCouponExpiryReminder(couponId, 1);
}
}

3️⃣ 用户激活提醒#

@Service
public class UserActivationService {
/**
* 用户注册后激活提醒
*/
public void sendActivationReminders(Long userId) {
// 24小时后首次提醒
delayMessageService.sendDelayMessage(
"user.activation.exchange",
"activation.remind.1day",
userId,
24 * 60 * 60
);
// 72小时后二次提醒
delayMessageService.sendDelayMessage(
"user.activation.exchange",
"activation.remind.3days",
userId,
72 * 60 * 60
);
// 7天后最终提醒
delayMessageService.sendDelayMessage(
"user.activation.exchange",
"activation.remind.7days",
userId,
7 * 24 * 60 * 60
);
}
@RabbitListener(queues = "activation.remind.queue")
public void handleActivationReminder(Long userId, @Header("x-death") List<Map<String, Object>> xDeath) {
User user = userService.getById(userId);
if (user != null && !user.isActivated()) {
// 发送激活提醒邮件
emailService.sendActivationReminder(user.getEmail());
log.info("发送用户{}激活提醒", userId);
}
}
}

🔄 分段延迟消息的高级模式#

智能分段策略#

@Service
public class SmartDelayMessageService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 发送分段延迟消息,支持中途取消
*/
public void sendSegmentedDelayTask(String taskId, int totalMinutes) {
// 分段策略:5分钟一段
int segmentMinutes = 5;
int segments = totalMinutes / segmentMinutes;
for (int i = 1; i <= segments; i++) {
DelayTaskMessage message = DelayTaskMessage.builder()
.taskId(taskId)
.segment(i)
.totalSegments(segments)
.action("CHECK_TASK_STATUS")
.build();
delayMessageService.sendDelayMessage(
"task.delay.exchange",
"task.segment.check",
message,
segmentMinutes * i * 60
);
}
// 在Redis中标记任务状态
redisTemplate.opsForValue().set(
"task:" + taskId + ":status",
"RUNNING",
Duration.ofMinutes(totalMinutes + 10)
);
log.info("任务{}已创建,共{}段,每{}分钟检查一次", taskId, segments, segmentMinutes);
}
/**
* 取消延迟任务
*/
public void cancelDelayTask(String taskId) {
redisTemplate.opsForValue().set("task:" + taskId + ":status", "CANCELLED");
log.info("任务{}已取消", taskId);
}
/**
* 处理分段延迟消息
*/
@RabbitListener(queues = "task.segment.check.queue")
public void handleSegmentCheck(DelayTaskMessage message) {
String taskId = message.getTaskId();
String statusKey = "task:" + taskId + ":status";
String status = (String) redisTemplate.opsForValue().get(statusKey);
// 检查任务是否已取消或完成
if (!"RUNNING".equals(status)) {
log.info("任务{}状态为{},跳过段{}检查", taskId, status, message.getSegment());
return;
}
// 执行具体的检查逻辑
boolean taskCompleted = executeTaskCheck(taskId, message);
if (taskCompleted) {
redisTemplate.opsForValue().set(statusKey, "COMPLETED");
log.info("任务{}已完成", taskId);
} else if (message.getSegment().equals(message.getTotalSegments())) {
// 最后一段,任务超时
redisTemplate.opsForValue().set(statusKey, "TIMEOUT");
handleTaskTimeout(taskId);
log.warn("任务{}执行超时", taskId);
}
}
private boolean executeTaskCheck(String taskId, DelayTaskMessage message) {
// 根据taskId和action执行具体的检查逻辑
switch (message.getAction()) {
case "CHECK_TASK_STATUS":
return checkTaskStatus(taskId);
case "CHECK_PAYMENT_STATUS":
return checkPaymentStatus(taskId);
default:
return false;
}
}
}

📊 延迟消息监控与告警#

监控指标配置#

@Component
public class DelayMessageMonitor {
private final MeterRegistry meterRegistry;
private final Counter delayMessageSentCounter;
private final Timer delayMessageProcessingTimer;
public DelayMessageMonitor(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.delayMessageSentCounter = Counter.builder("delay.message.sent")
.description("延迟消息发送计数")
.register(meterRegistry);
this.delayMessageProcessingTimer = Timer.builder("delay.message.processing")
.description("延迟消息处理时间")
.register(meterRegistry);
}
/**
* 记录延迟消息发送
*/
public void recordDelayMessageSent(String exchange, String routingKey, int delaySeconds) {
delayMessageSentCounter.increment(
Tags.of(
"exchange", exchange,
"routing_key", routingKey,
"delay_range", getDelayRange(delaySeconds)
)
);
}
/**
* 记录延迟消息处理时间
*/
public void recordDelayMessageProcessing(String queue, Runnable task) {
Timer.Sample sample = Timer.start(meterRegistry);
try {
task.run();
} finally {
sample.stop(delayMessageProcessingTimer.tag("queue", queue));
}
}
private String getDelayRange(int seconds) {
if (seconds < 60) return "0-1min";
if (seconds < 300) return "1-5min";
if (seconds < 1800) return "5-30min";
if (seconds < 3600) return "30min-1h";
return "1h+";
}
}

🚨 延迟消息的注意事项#

性能优化建议#

  1. 控制延迟时长:避免超过1小时的延迟
  2. 分段处理:长任务分解为多个短延迟
  3. 状态管理:使用Redis管理任务状态,支持取消
  4. 监控告警:监控延迟消息的堆积情况
  5. 幂等设计:确保重复消息的安全处理

故障处理方案#

@Service
public class DelayMessageFailureHandler {
/**
* 延迟消息失败重试策略
*/
@RabbitListener(queues = "delay.message.retry.queue")
public void handleDelayMessageRetry(DelayTaskMessage message,
@Header Map<String, Object> headers) {
try {
// 获取重试次数
Integer retryCount = (Integer) headers.getOrDefault("x-retry-count", 0);
if (retryCount >= 3) {
// 重试次数过多,转入人工处理队列
rabbitTemplate.convertAndSend("manual.process.exchange",
"delay.message.failed", message);
log.error("延迟消息重试失败,转入人工处理:{}", message);
return;
}
// 处理延迟消息
processDelayMessage(message);
} catch (Exception e) {
log.error("处理延迟消息失败,准备重试:{}", message, e);
// 增加重试次数并重新发送
headers.put("x-retry-count", (Integer) headers.getOrDefault("x-retry-count", 0) + 1);
rabbitTemplate.convertAndSend("delay.message.retry.exchange",
"retry", message, msg -> {
headers.forEach((key, value) ->
msg.getMessageProperties().getHeaders().put(key, value));
// 重试延迟递增:2^retry_count 分钟
int delayMinutes = (int) Math.pow(2, (Integer) headers.get("x-retry-count"));
msg.getMessageProperties().setHeader("x-delay", delayMinutes * 60 * 1000);
return msg;
});
}
}
}
## 4.5 订单状态同步问题(实战案例)
### 🎯 业务背景
在交易服务中利用延迟消息实现订单支付状态同步,解决订单超时未支付的问题。
#### 📋 优化思路分析
**传统做法**:订单超时30分钟,发送一条30分钟延迟消息
- ❌ **问题**:大多数用户1分钟内支付,消息却要在MQ中停留30分钟
- ❌ **资源浪费**:长时间占用MQ资源
**优化方案**:多次检测,而不是最后一刻才检测
- ✅ **优点**:提前发现支付完成,取消后续检测
- ✅ **节省资源**:避免长时间占用MQ
**优化后的检测时间点**:
- 10秒、20秒、30秒、45秒、60秒
- 1分30秒、2分钟、5分钟、10分钟、30分钟
### 📊 多延迟消息数据结构
#### 通用多延迟消息实体
```java
package com.hmall.common.domain;
import lombok.Data;
import java.util.List;
@Data
public class MultiDelayMessage<T> {
/**
* 消息体
*/
private T data;
/**
* 记录延迟时间的集合(毫秒)
*/
private List<Long> delayMillis;
public MultiDelayMessage(T data, List<Long> delayMillis) {
this.data = data;
this.delayMillis = delayMillis;
}
/**
* 便捷构造方法
*/
public static <T> MultiDelayMessage<T> of(T data, Long... delayMillis) {
return new MultiDelayMessage<>(data, Arrays.asList(delayMillis));
}
/**
* 获取并移除下一个延迟时间
*/
public Long removeNextDelay() {
return delayMillis.remove(0);
}
/**
* 是否还有下一个延迟时间
*/
public boolean hasNextDelay() {
return !delayMillis.isEmpty();
}
}

🔧 系统集成配置#

1)定义MQ常量#

package com.hmall.trade.constants;
public interface MqConstants {
String DELAY_EXCHANGE = "trade.delay.topic";
String DELAY_ORDER_QUEUE = "trade.order.delay.queue";
String DELAY_ORDER_ROUTING_KEY = "order.query";
}

2)抽取共享MQ配置#

在nacos中定义shared-mq.yml:

spring:
rabbitmq:
host: ${hm.mq.host:192.168.150.101}
port: ${hm.mq.port:5672}
virtual-host: ${hm.mq.vhost:/hmall}
username: ${hm.mq.un:hmall}
password: ${hm.mq.pw:123}
listener:
simple:
prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息

💼 业务实现#

1)改造下单业务#

@Service
public class OrderServiceImpl implements IOrderService {
@Autowired
private RabbitTemplate rabbitTemplate;
@Override
@Transactional
public Long createOrder(OrderFormDTO orderFormDTO) {
// 1. 创建订单
Order order = buildOrder(orderFormDTO);
save(order);
// 2. 清理购物车
clearCart(orderFormDTO.getCartIds());
// 3. 扣减库存
orderDetailService.reduceStock(order.getId(), orderFormDTO.getDetails());
// 4. 发送延迟消息检查支付状态
sendOrderDelayMessage(order.getId());
return order.getId();
}
/**
* 发送订单延迟检查消息
*/
private void sendOrderDelayMessage(Long orderId) {
// 定义检查时间点(秒):10s, 30s, 60s, 5min, 15min, 30min
MultiDelayMessage<Long> message = MultiDelayMessage.of(
orderId,
10 * 1000L, // 10秒
30 * 1000L, // 30秒
60 * 1000L, // 1分钟
5 * 60 * 1000L, // 5分钟
15 * 60 * 1000L, // 15分钟
30 * 60 * 1000L // 30分钟
);
// 发送第一次延迟消息
rabbitTemplate.convertAndSend(
MqConstants.DELAY_EXCHANGE,
MqConstants.DELAY_ORDER_ROUTING_KEY,
message,
msg -> {
msg.getMessageProperties().setHeader("x-delay", message.removeNextDelay().intValue());
return msg;
}
);
log.info("订单{}延迟检查消息已发送", orderId);
}
}

2)支付状态查询接口#

PayClient.java
@FeignClient(value = "pay-service", fallbackFactory = PayClientFallback.class)
public interface PayClient {
/**
* 根据交易订单id查询支付单
*/
@GetMapping("/pay-orders/biz/{id}")
PayOrderDTO queryPayOrderByBizOrderNo(@PathVariable("id") Long id);
}
// PayController.java - pay-service中实现
@GetMapping("/biz/{id}")
public PayOrderDTO queryPayOrderByBizOrderNo(@PathVariable("id") Long id) {
PayOrder payOrder = payOrderService.lambdaQuery()
.eq(PayOrder::getBizOrderNo, id)
.one();
return BeanUtils.copyBean(payOrder, PayOrderDTO.class);
}

3)延迟消息监听器#

@Slf4j
@Component
@RequiredArgsConstructor
public class OrderStatusListener {
private final IOrderService orderService;
private final PayClient payClient;
private final RabbitTemplate rabbitTemplate;
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = MqConstants.DELAY_ORDER_QUEUE, durable = "true"),
exchange = @Exchange(name = MqConstants.DELAY_EXCHANGE, type = ExchangeTypes.TOPIC),
key = MqConstants.DELAY_ORDER_ROUTING_KEY
))
public void listenOrderCheckDelayMessage(MultiDelayMessage<Long> msg) {
// 1. 获取订单ID
Long orderId = msg.getData();
log.info("开始检查订单{}的支付状态", orderId);
// 2. 查询订单状态
Order order = orderService.getById(orderId);
if (order == null || order.getStatus() > 1) {
log.info("订单{}不存在或已处理,跳过检查", orderId);
return;
}
// 3. 查询支付状态
PayOrderDTO payOrder = null;
try {
payOrder = payClient.queryPayOrderByBizOrderNo(orderId);
} catch (Exception e) {
log.error("查询订单{}支付状态失败", orderId, e);
}
// 4. 判断支付状态
if (payOrder != null && payOrder.getStatus() == 3) {
// 支付成功,更新订单状态
orderService.markOrderPaySuccess(orderId);
log.info("订单{}支付成功,状态已更新", orderId);
return;
}
// 5. 未支付,判断是否继续检查
if (msg.hasNextDelay()) {
// 发送下一次延迟检查消息
int nextDelay = msg.removeNextDelay().intValue();
rabbitTemplate.convertAndSend(
MqConstants.DELAY_EXCHANGE,
MqConstants.DELAY_ORDER_ROUTING_KEY,
msg,
message -> {
message.getMessageProperties().setHeader("x-delay", nextDelay);
return message;
}
);
log.info("订单{}将在{}秒后进行下次检查", orderId, nextDelay / 1000);
} else {
// 没有剩余检查时间,取消订单
orderService.cancelOrder(orderId);
log.warn("订单{}超时未支付,已取消", orderId);
}
}
}

🔧 订单取消业务实现#

取消订单方法#

@Override
@Transactional
public void cancelOrder(Long orderId) {
// 1. 查询订单
Order order = getById(orderId);
if (order == null) {
log.warn("订单{}不存在", orderId);
return;
}
// 2. 幂等性检查
if (order.getStatus() != 1) {
log.info("订单{}状态为{},不是未支付状态,跳过取消", orderId, order.getStatus());
return;
}
// 3. 更新订单状态为已取消
boolean updated = lambdaUpdate()
.set(Order::getStatus, 5) // 5-已取消
.set(Order::getCloseTime, LocalDateTime.now())
.eq(Order::getId, orderId)
.eq(Order::getStatus, 1) // 只有未支付状态才能取消
.update();
if (!updated) {
log.info("订单{}取消失败,可能状态已变更", orderId);
return;
}
// 4. 恢复库存
restoreInventory(orderId);
log.info("订单{}已成功取消并恢复库存", orderId);
}
/**
* 恢复库存
*/
private void restoreInventory(Long orderId) {
// 查询订单详情
List<OrderDetail> details = orderDetailService.lambdaQuery()
.eq(OrderDetail::getOrderId, orderId)
.list();
// 批量恢复库存
for (OrderDetail detail : details) {
inventoryService.restoreStock(detail.getItemId(), detail.getNum());
}
}

📊 完整方案总结#

🔄 工作流程#

graph TD A[用户下单] --> B[创建订单] B --> C[发送第1次延迟消息 10s] C --> D{10s后检查} D -->|已支付| E[更新订单状态] D -->|未支付| F[发送第2次延迟消息 30s] F --> G{30s后检查} G -->|已支付| E G -->|未支付| H[继续后续检查...] H --> I{30分钟后最终检查} I -->|已支付| E I -->|未支付| J[取消订单并恢复库存]

📈 性能优化效果#

指标传统方案优化方案改善效果
平均延迟消息存活时间30分钟~1分钟降低96%
MQ资源占用高低显著降低
支付状态检测实时性低高显著提升
用户体验一般优秀明显改善

🎯 方案优势#

  1. 资源节省:大部分消息在1分钟内完成,避免长时间占用MQ
  2. 实时性强:多次检查提高支付状态更新的实时性
  3. 用户体验好:支付后快速反馈,无需等待
  4. 系统稳定:分段检查降低系统压力
  5. 易于扩展:可根据业务需求调整检查时间点

这种设计既保证了业务需求,又优化了系统性能,是延迟消息在实际业务中的最佳实践。


🎯 本章总结#

📚 核心知识回顾#

1️⃣ 生产者可靠性机制#

  • 生产者确认机制:确保消息成功到达MQ
  • 生产者重试机制:网络故障时自动重试
  • 生产者确认回调:实时感知发送结果

2️⃣ MQ可靠性保障#

  • 数据持久化:Exchange、Queue、Message三重持久化
  • LazyQueue机制:内存+磁盘混合存储,提升性能
  • 集群容错:多节点保障高可用

3️⃣ 消费者可靠性策略#

  • 消费者确认机制:manual模式精确控制
  • 消费者重试机制:本地重试+失败策略
  • 消费者限流:prefetch控制并发度

4️⃣ 延迟消息实现#

  • DelayExchange插件:官方推荐方案
  • 死信+TTL组合:传统实现方式
  • 业务场景应用:订单超时、定时任务

🛡️ 可靠性最佳实践#

🔄 完整可靠性架构#

graph TD A[业务服务] -->|发送消息| B[RabbitMQ Broker] B -->|确认回调| A B -->|持久化存储| C[磁盘存储] B -->|消息投递| D[消费者服务] D -->|消费确认| B D -->|处理失败| E[重试机制] E -->|超出重试次数| F[失败队列] F -->|人工干预| G[补偿处理]

📋 可靠性配置清单#

环节配置项推荐值作用
生产者publisher-confirm-typecorrelated开启确认机制
publisher-returnstrue开启退回机制
template.mandatorytrue强制路由
MQdurabletrue交换机持久化
durabletrue队列持久化
deliveryMode2消息持久化
消费者acknowledge-modemanual手动确认
prefetch1限流控制
retry.enabledtrue开启重试

⚡ 性能优化策略#

📊 性能对比分析#

优化方案吞吐量提升延迟降低资源节省
LazyQueue30-50%20-30%60-80%
批量发送200-300%-40-60%
合理prefetch100-200%50-70%30-50%
连接池优化50-100%30-50%20-40%

🚀 高性能配置#

spring:
rabbitmq:
# 连接池配置
cache:
connection:
mode: channel
size: 10
# 生产者配置
template:
retry:
enabled: true
max-attempts: 3
# 消费者配置
listener:
simple:
prefetch: 10 # 根据处理能力调整
concurrency: 2 # 并发消费者数量
max-concurrency: 10 # 最大并发数

🔧 故障排查指南#

🚨 常见问题诊断#

  1. 消息丢失排查

    # 检查队列消息积压
    rabbitmqctl list_queues name messages
    # 检查交换机绑定
    rabbitmqctl list_bindings
    # 查看连接状态
    rabbitmqctl list_connections
  2. 性能问题分析

    # 监控队列状态
    rabbitmqctl list_queues name messages_ready messages_unacknowledged
    # 查看消费者状态
    rabbitmqctl list_consumers
    # 监控内存使用
    rabbitmq-diagnostics memory_breakdown
  3. 重试机制验证

    // 日志中查看重试记录
    2024-01-15 10\:30\:00 WARN Retrying message delivery, attempt: 2/3
    2024-01-15 10\:30\:05 ERROR Message delivery failed after 3 attempts, sending to DLQ

📈 监控与告警#

📊 关键指标监控#

graph LR A[RabbitMQ监控] --> B[队列指标] A --> C[连接指标] A --> D[节点指标] A --> E[业务指标] B --> B1[消息堆积] B --> B2[消费速率] B --> B3[确认延迟] C --> C1[连接数量] C --> C2[信道数量] C --> C3[连接状态] D --> D1[内存使用] D --> D2[磁盘使用] D --> D3[CPU使用率] E --> E1[业务成功率] E --> E2[处理延迟] E --> E3[错误率]

⚠️ 告警规则配置#

指标告警阈值级别处理建议
队列消息堆积> 1000条WARNING检查消费者状态
消息堆积> 10000条CRITICAL立即扩容消费者
消费延迟> 5秒WARNING优化处理逻辑
错误率> 5%CRITICAL检查业务逻辑
连接数> 80%最大值WARNING优化连接池

🎯 实战应用场景#

💼 典型业务场景#

  1. 订单系统

    • 订单超时取消(延迟消息)
    • 库存扣减确认(可靠性保证)
    • 支付状态同步(重试机制)
  2. 用户系统

    • 注册邮件发送(异步处理)
    • 密码重置通知(可靠投递)
    • 用户行为分析(数据收集)
  3. 物流系统

    • 配送状态更新(实时同步)
    • 异常订单处理(失败重试)
    • 运费计算通知(延迟处理)

🏗️ 架构设计模式#

graph TD A[前端应用] --> B[API网关] B --> C[业务服务集群] C --> D[RabbitMQ集群] D --> E[消费者服务集群] E --> F[数据存储层] D --> G[监控告警系统] G --> H[运维平台] style D fill:#e1f5fe style E fill:#f3e5f5 style G fill:#fff3e0

📝 开发规范建议#

✅ 编码最佳实践#

  1. 消息设计原则

    • 消息体尽量小巧,避免大对象传输
    • 使用合适的序列化方式(JSON/Protobuf)
    • 设计幂等性消息处理逻辑
  2. 异常处理策略

    @RabbitListener(queues = "business.queue")
    public void handleMessage(BusinessMessage msg) {
    try {
    // 业务处理逻辑
    businessService.process(msg);
    } catch (BusinessException e) {
    // 业务异常,记录日志,不重试
    log.error("Business error: {}", e.getMessage());
    throw new AmqpRejectAndDontRequeueException(e);
    } catch (Exception e) {
    // 系统异常,可以重试
    log.error("System error: {}", e.getMessage(), e);
    throw e;
    }
    }
  3. 性能优化建议

    • 合理设置prefetch值
    • 使用LazyQueue减少内存占用
    • 批量处理提升吞吐量
    • 连接池复用减少开销

🔮 技术发展趋势#

🌟 新特性展望#

  1. Quorum队列:更强的一致性保证
  2. Stream队列:流式数据处理支持
  3. 插件生态:更丰富的功能扩展
  4. 云原生支持:Kubernetes原生集成

🎓 持续学习路径#

  1. 深入原理:AMQP协议、Erlang/OTP平台
  2. 高级特性:Federation、Shovel、插件开发
  3. 运维实践:集群管理、性能调优、故障处理
  4. 生态集成:Spring Cloud Stream、Kafka对比

通过本章的学习,你已经掌握了RabbitMQ高级特性的核心知识。在实际项目中,请根据业务需求选择合适的可靠性策略,并持续监控和优化系统性能。记住,可靠性和性能往往需要权衡,选择最适合业务场景的方案才是最好的方案。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

RabbitMq高级特性
https://xinrose.top/posts/mq/rabbitmq-high/
作者
xinRose
发布于
2025-07-18
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录