前言:从 "基础投递" 到 "场景适配",解锁 RocketMQ 高级处理能力
在前三篇文章中,我们掌握了 RocketMQ 高可用集群部署、消息追踪与性能优化、分布式事务消息三大核心能力,实现了集群的稳定高效运行 和分布式数据一致性保障。但在实际生产场景中,业务需求远不止 "消息可靠投递"------ 我们需要更精细化的消息处理能力:
- 电商场景中,订单消息需要按 "订单类型"(实物订单、虚拟订单)分发给不同消费者,避免消息混淆;
- 直播带货场景中,需要延迟发送 "优惠券发放消息"(如开播后 5 分钟发送),提升用户参与感;
- 支付场景中,可能出现 "支付消息消费失败" 的情况,若不及时处理会导致用户权益受损,需要兜底机制保障消息最终处理。
这些场景的实现,依赖 RocketMQ 的三大高级特性:消息过滤、延迟消息、死信队列。它们分别解决了 "消息精细化路由"、"定时调度"、"异常消息兜底" 三大核心需求,是 RocketMQ 从 "通用消息中间件" 到 "业务适配型中间件" 的关键升级。
本篇作为高级篇的第四篇,将从场景化应用角度,深度解析这三大特性的实现原理、实战代码与生产避坑技巧,结合电商、直播、支付等典型业务场景,帮你彻底掌握 RocketMQ 的高级消息处理能力,让 RocketMQ 能适配更复杂、更精细化的分布式业务需求。
前置要求 :
已掌握 RocketMQ 高可用集群(Dledger 模式)部署与基础运维方法
具备 Spring Boot 集成 RocketMQ 的开发经验,熟悉消息发送与消费的基本流程
了解分布式业务场景中 "精细化路由"、"定时任务"、"异常兜底" 的核心需求
已搭建 RocketMQ 官方控制台,可查看消息状态、过滤结果、延迟消息状态等信息
一、消息过滤实战:实现消息精细化路由,降低业务冗余
1.1 核心痛点与应用场景
普通消息投递是 "生产者发送→Broker 存储→所有消费者订阅接收",但实际业务中,同一 Topic 下的消息需要按不同规则分发给不同消费者 ,例如:
电商订单 Topic(order-topic):实物订单需投递到 "物流服务消费者",虚拟订单需投递到 "充值服务消费者";
通知 Topic(notice-topic):紧急通知需投递到 "紧急处理消费者",普通通知投递到 "普通处理消费者"。
若不做过滤,所有消费者都会接收全部消息,造成:
消费者处理冗余:消费者需要额外过滤无效消息,浪费 CPU 资源;
网络带宽浪费:无效消息在网络中传输,增加带宽开销;
业务耦合:消费者需要感知消息业务规则,代码复杂度提升。
RocketMQ 消息过滤正是解决这一问题的核心方案,它允许消费者按 Tag、SQL 表达式过滤消息,只接收符合条件的消息,实现精细化路由。
bash
在这里插入代码片
1.2 两种过滤方案对比与选型
RocketMQ 支持两种消息过滤方式,分别适用于不同场景,核心区别在于过滤执行节点和支持的过滤规则:
| 过滤方案 | 过滤执行节点 | 支持的规则 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|---|---|
| Tag 过滤 | Broker 端 | 简单标签(如 "order-logistics"、"order-recharge") | 性能高、开发简单、无需额外配置 | 仅支持单标签 / 多标签匹配,规则简单 | 大多数业务场景(如按业务类型、优先级过滤) |
| SQL 过滤 | 消费者端 | 复杂 SQL 表达式(如 age > 18 AND status = 'valid') | 规则灵活、支持复杂条件判断 | 性能略低、需开启 Broker 配置、依赖消费者解析 | 复杂业务场景(如按多属性、范围、逻辑运算过滤) |
选型建议
优先使用 Tag 过滤:适用于 90% 的业务场景,规则简单、性能最优,无需额外配置;
复杂场景使用 SQL 过滤:仅当 Tag 无法满足规则时(如按消息属性范围、多条件组合过滤)使用,需注意性能损耗。
1.3 实战一:Tag 过滤(电商订单场景)
1.3.1 核心原理
Tag 过滤的核心逻辑是:
生产者发送消息时,为消息指定 Tag(如 "logistics"、"recharge");
消费者订阅 Topic 时,指定需要过滤的 Tag(如 "logistics"、"recharge");
Broker 端根据消费者的 Tag 配置,直接过滤掉不符合条件的消息,只投递符合条件的消息到对应消费者。
1.3.2 实战代码(Spring Boot 集成)
步骤 1:生产者开发(发送带 Tag 的消息)
bash
package com.example.orderservice.producer;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class OrderTagProducer {
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 发送带 Tag 的订单消息
* @param orderNo 订单编号
* @param orderType 订单类型:logistics-实物订单,recharge-虚拟订单
*/
public void sendOrderTagMessage(String orderNo, String orderType) {
// Topic: order-topic,Tag: 订单类型(如 logistics、recharge)
// 消息体:订单编号
rocketMQTemplate.convertAndSend("order-topic:" + orderType, orderNo);
System.out.println("发送订单消息成功,订单编号:" + orderNo + ",订单类型:" + orderType);
}
}
步骤 2:消费者开发(按 Tag 订阅消息)
实物订单消费者(订阅 logistics Tag)
c
package com.example.orderservice.consumer;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;
// 订阅 order-topic 主题,只接收 Tag 为 logistics 的消息
@RocketMQMessageListener(
topic = "order-topic",
selectorExpression = "logistics", // Tag 过滤规则:只接收 logistics 标签的消息
consumerGroup = "logistics-consumer-group"
)
@Component
public class LogisticsConsumer implements RocketMQListener<String> {
@Override
public void onMessage(String orderNo) {
System.out.println("【实物订单消费者】处理物流订单,订单编号:" + orderNo);
// 执行物流处理逻辑:生成物流单、通知仓库发货
handleLogistics(orderNo);
}
// 模拟物流处理逻辑
private void handleLogistics(String orderNo) {
// 实际业务:调用物流接口、生成物流单
System.out.println("订单" + orderNo + ":生成物流单、通知仓库发货完成");
}
}
虚拟订单消费者(订阅 recharge Tag)
bash
package com.example.orderservice.consumer;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;
// 订阅 order-topic 主题,只接收 Tag 为 recharge 的消息
@RocketMQMessageListener(
topic = "order-topic",
selectorExpression = "recharge", // Tag 过滤规则:只接收 recharge 标签的消息
consumerGroup = "recharge-consumer-group"
)
@Component
public class RechargeConsumer implements RocketMQListener<String> {
@Override
public void onMessage(String orderNo) {
System.out.println("【虚拟订单消费者】处理充值订单,订单编号:" + orderNo);
// 执行充值处理逻辑:发放虚拟商品、通知用户充值到账
handleRecharge(orderNo);
}
// 模拟充值处理逻辑
private void handleRecharge(String orderNo) {
// 实际业务:调用充值接口、发放虚拟商品
System.out.println("订单" + orderNo + ":发放虚拟商品、通知用户到账完成");
}
}
步骤 3:测试验证
调用生产者发送两种类型的订单消息:
bash
// 测试代码:发送实物订单和虚拟订单
@Autowired
private OrderTagProducer orderTagProducer;
// 发送实物订单
orderTagProducer.sendOrderTagMessage("ORDER20240401001", "logistics");
// 发送虚拟订单
orderTagProducer.sendOrderTagMessage("ORDER20240401002", "recharge");
预期结果 :
实物订单(logistics Tag)仅被 LogisticsConsumer 接收处理;
虚拟订单(recharge Tag)仅被 RechargeConsumer 接收处理;
两个消费者不会互相接收对方的消息,实现精准过滤。
1.3.3 Tag 过滤进阶:多 Tag 匹配
若需要让消费者接收多个 Tag 的消息,可在 selectorExpression 中用 ** 竖线(|)** 分隔 Tag:
bash
// 订阅 order-topic 主题,接收 logistics 或 recharge Tag 的消息
@RocketMQMessageListener(
topic = "order-topic",
selectorExpression = "logistics|recharge", // 多 Tag 匹配
consumerGroup = "order-all-consumer-group"
)
@Component
public class OrderAllConsumer implements RocketMQListener<String> {
// 处理逻辑:同时处理实物订单和虚拟订单
}
1.4 实战二:SQL 过滤(复杂业务场景)
1.4.1 核心原理
SQL 过滤允许消费者按 消息属性(Properties) 过滤消息,支持复杂的 SQL 表达式(如 age > 18 AND score >= 90),核心流程:
1.生产者发送消息时,为消息添加自定义属性(Properties);
2.开启 Broker 端 SQL 过滤功能(需修改 broker.conf);
3.消费者订阅 Topic 时,指定 SQL 过滤规则;
4.消费者解析 SQL 规则,过滤出符合条件的消息并消费。
1.4.2 前置配置:开启 Broker SQL 过滤
修改 RocketMQ Broker 配置文件(broker.conf),添加以下配置:
bash
# 开启 SQL 过滤功能(默认关闭)
enablePropertyFilter = true
重启 Broker 集群,使配置生效。
1.4.3 实战代码(按用户年龄 + 积分过滤消息)
步骤 1:生产者开发(添加消息属性)
bash
package com.example.userservice.producer;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class UserMsgProducer {
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 发送带属性的用户消息
* @param userId 用户ID
* @param age 用户年龄
* @param score 用户积分
*/
public void sendUserMsg(String userId, int age, int score) {
// 1. 创建消息对象
org.apache.rocketmq.common.message.Message message =
new org.apache.rocketmq.common.message.Message(
"user-topic", // Topic
"user-tag", // Tag(可与 SQL 过滤结合使用)
userId.getBytes() // 消息体
);
// 2. 添加自定义属性(用于 SQL 过滤)
message.putUserProperty("age", String.valueOf(age));
message.putUserProperty("score", String.valueOf(score));
// 3. 发送消息
rocketMQTemplate.send(message);
System.out.println("发送用户消息成功,用户ID:" + userId + ",年龄:" + age + ",积分:" + score);
}
}
步骤 2:消费者开发(按 SQL 规则过滤)
高级用户消费者(年龄≥18 且积分≥90)
bash
package com.example.userservice.consumer;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.apache.rocketmq.common.message.MessageExt;
import org.springframework.stereotype.Component;
// 订阅 user-topic 主题,按 SQL 规则过滤消息
@RocketMQMessageListener(
topic = "user-topic",
selectorType = RocketMQMessageListener.SelectorType.SQL92, // 开启 SQL 过滤
selectorExpression = "age >= 18 AND score >= 90", // SQL 过滤规则
consumerGroup = "vip-user-consumer-group"
)
@Component
public class VipUserConsumer implements RocketMQListener<MessageExt> {
@Override
public void onMessage(MessageExt message) {
String userId = new String(message.getBody());
System.out.println("【高级用户消费者】处理高级用户消息,用户ID:" + userId);
// 执行高级用户专属逻辑:发放专属权益、推送专属通知
handleVipUser(userId);
}
private void handleVipUser(String userId) {
System.out.println("用户" + userId + ":发放专属权益、推送专属通知完成");
}
}
普通用户消费者(年龄 < 18 或积分 < 90)
bash
package com.example.userservice.consumer;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.apache.rocketmq.common.message.MessageExt;
import org.springframework.stereotype.Component;
@RocketMQMessageListener(
topic = "user-topic",
selectorType = RocketMQMessageListener.SelectorType.SQL92,
selectorExpression = "age < 18 OR score < 90", // 普通用户规则
consumerGroup = "normal-user-consumer-group"
)
@Component
public class NormalUserConsumer implements RocketMQListener<MessageExt> {
@Override
public void onMessage(MessageExt message) {
String userId = new String(message.getBody());
System.out.println("【普通用户消费者】处理普通用户消息,用户ID:" + userId);
// 执行普通用户逻辑:发放普通权益、推送普通通知
handleNormalUser(userId);
}
private void handleNormalUser(String userId) {
System.out.println("用户" + userId + ":发放普通权益、推送普通通知完成");
}
}
步骤 3:测试验证
调用生产者发送不同属性的用户消息:
bash
// 测试代码:发送高级用户和普通用户消息
@Autowired
private UserMsgProducer userMsgProducer;
// 高级用户:年龄20,积分95
userMsgProducer.sendUserMsg("USER20240401001", 20, 95);
// 普通用户:年龄17,积分80
userMsgProducer.sendUserMsg("USER20240401002", 17, 80);
// 边缘用户:年龄25,积分85
userMsgProducer.sendUserMsg("USER20240401003", 25, 85);
预期结果:
1.高级用户消息(age=20, score=95)仅被 VipUserConsumer 接收;
2.普通用户消息(age=17, score=80)仅被 NormalUserConsumer 接收;
3.边缘用户消息(age=25, score=85)未被任何消费者接收(无匹配规则),可通过死信队列处理(后续讲解)。
1.4.4 SQL 过滤支持的语法与注意事项
支持的语法
RocketMQ SQL 过滤支持 92 标准 SQL 语法 ,包括:
比较运算符:>、<、>=、<=、=、<>
逻辑运算符:AND、OR、NOT
数学运算符:+、-、*、/
常量类型:字符串(单引号)、数字(整数、小数)
注意事项
1.消息属性必须为字符串类型 ,需手动转换(如 int 转 String);
- SQL 过滤性能低于 Tag 过滤 ,高并发场景下优先使用 Tag 过滤;
3.不支持复杂的函数调用(如 SUBSTR(age, 1, 2)),仅支持基础运算;
4.需确保 Broker 开启 enablePropertyFilter = true,否则 SQL 过滤无效。
二、延迟消息实战:实现业务定时调度,适配直播 / 预约场景
2.1 核心痛点与应用场景
在分布式业务中,经常需要延迟执行某些操作,例如:
- 直播场景:开播后 5 分钟自动发送优惠券消息,提升用户停留时长;
- 电商场景:订单创建后 30 分钟未支付,自动取消订单并释放库存;
- 预约场景:用户预约活动开始前 1 小时,自动发送提醒消息。
传统解决方案是定时任务(如 Quartz、XXL-Job),但存在两个核心问题:
-
时效性差:定时任务通常按分钟/小时调度,无法实现毫秒级延迟;
-
资源浪费:大量定时任务占用数据库或调度服务资源,高并发场景下易出现调度瓶颈;
-
耦合度高:定时任务与业务逻辑绑定,代码维护成本高,且不易扩展。
RocketMQ 延迟消息完美解决了这些问题,它允许生产者发送消息时指定延迟时间,Broker 会在延迟时间到达后,自动将消息投递到消费者,实现"定时调度"功能,且依托 RocketMQ 集群的高可用性,无需额外搭建调度服务,性能更优、耦合度更低。
2.2 延迟消息核心原理
RocketMQ 延迟消息的核心逻辑的是"延迟队列+定时投递",具体流程如下:
-
生产者发送延迟消息时,指定延迟级别(而非具体延迟时间);
-
Broker 接收消息后,不会立即将其存入目标 Topic 的队列,而是存入内置延迟队列(SCHEDULE_TOPIC_XXXX);
-
Broker 内部有定时任务,定期扫描延迟队列,判断消息延迟时间是否到达;
-
若延迟时间到达,Broker 将消息从延迟队列中取出,投递到目标 Topic 的队列,供消费者消费;
-
若延迟时间未到达,继续存放在延迟队列中,等待下一次扫描。
关键说明:延迟级别
RocketMQ 延迟消息不支持自定义延迟时间,仅支持预设的延迟级别,默认延迟级别如下(可通过 Broker 配置修改):
bash
# 默认延迟级别(18个级别)
messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
对应关系:延迟级别 1 → 1秒,级别 2 → 5秒,...,级别 18 → 2小时。
若需要自定义延迟时间,可修改 Broker 配置文件(broker.conf)中的 messageDelayLevel,添加或调整延迟级别(如添加 30s 1m 5m),重启 Broker 生效。
注意事项:
-
延迟级别从 1 开始计数,不可跳过级别(如不能直接指定级别 0 或级别 19);
-
延迟消息的延迟时间是"消息发送到 Broker 后开始计算",而非"消费者接收消息后开始计算";
-
延迟消息不支持重试机制(若消费失败,会进入死信队列),需提前做好异常处理。
2.3 实战一:电商场景------订单30分钟未支付自动取消
核心需求:订单创建后,若30分钟内未支付,自动取消订单、释放库存,通过延迟消息实现定时触发。
2.3.1 实战代码(Spring Boot 集成)
步骤1:生产者开发(发送延迟消息)
订单创建成功后,发送延迟消息(延迟级别 16 → 30分钟),消息体为订单编号,用于后续取消订单。
bash
package com.example.orderservice.producer;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class OrderDelayProducer {
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 发送订单延迟消息(30分钟未支付自动取消)
* @param orderNo 订单编号
*/
public void sendOrderDelayMessage(String orderNo) {
// 延迟级别 16 → 30分钟(对应默认延迟级别 messageDelayLevel)
int delayLevel = 16;
// 发送延迟消息:topic=order-cancel-topic,消息体=订单编号,延迟级别=16
rocketMQTemplate.syncSend(
"order-cancel-topic",
orderNo,
3000, // 发送超时时间
delayLevel // 延迟级别
);
System.out.println("发送订单延迟消息成功,订单编号:" + orderNo + ",延迟时间:30分钟");
}
}
步骤2:订单创建接口(关联延迟消息)
在订单创建接口中,调用延迟消息生产者,发送延迟消息(若用户后续支付成功,需删除延迟消息,避免重复取消订单)。
bash
package com.example.orderservice.controller;
import com.example.orderservice.producer.OrderDelayProducer;
import com.example.orderservice.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@Autowired
private OrderDelayProducer orderDelayProducer;
// 订单创建接口
@PostMapping("/createOrder")
public String createOrder(@RequestParam String userId, @RequestParam Long productId) {
// 1. 创建订单(本地事务)
String orderNo = orderService.createOrder(userId, productId);
if (orderNo == null) {
return "订单创建失败";
}
// 2. 发送延迟消息(30分钟未支付自动取消)
orderDelayProducer.sendOrderDelayMessage(orderNo);
return "订单创建成功,订单编号:" + orderNo + ",30分钟未支付将自动取消";
}
// 订单支付接口(支付成功后,删除延迟消息)
@PostMapping("/payOrder")
public String payOrder(@RequestParam String orderNo) {
// 1. 执行支付逻辑
boolean payResult = orderService.payOrder(orderNo);
if (!payResult) {
return "支付失败";
}
// 2. 删除延迟消息(避免订单被自动取消)
// 注意:RocketMQ 不支持直接删除消息,需通过"标记消息无效"或"消费时过滤"实现
// 这里采用"消费时过滤":支付成功后,更新订单状态,消费延迟消息时判断状态
return "支付成功,订单编号:" + orderNo;
}
}
步骤3:消费者开发(处理延迟消息,取消订单)
延迟时间到达后,消费者接收消息,判断订单状态:若未支付,取消订单、释放库存;若已支付,直接忽略(幂等处理)。
bash
package com.example.orderservice.consumer;
import com.example.orderservice.service.OrderService;
import com.example.orderservice.service.InventoryService;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
// 订阅订单取消延迟消息
@RocketMQMessageListener(
topic = "order-cancel-topic",
consumerGroup = "order-cancel-consumer-group"
)
@Component
public class OrderCancelConsumer implements RocketMQListener<String> {
@Autowired
private OrderService orderService;
@Autowired
private InventoryService inventoryService;
@Override
public void onMessage(String orderNo) {
System.out.println("收到订单取消延迟消息,订单编号:" + orderNo);
// 1. 查询订单状态:0-待支付,1-已支付,2-已取消
Integer orderStatus = orderService.getOrderStatus(orderNo);
if (orderStatus == null) {
System.out.println("订单" + orderNo + "不存在,忽略取消操作");
return;
}
// 2. 若订单未支付,执行取消逻辑
if (orderStatus == 0) {
// 取消订单
boolean cancelResult = orderService.cancelOrder(orderNo);
if (cancelResult) {
// 释放库存(根据订单编号查询商品ID和数量)
Long productId = orderService.getProductIdByOrderNo(orderNo);
Integer productNum = orderService.getProductNumByOrderNo(orderNo);
inventoryService.releaseStock(productId, productNum);
System.out.println("订单" + orderNo + ":30分钟未支付,已自动取消并释放库存");
}
} else {
// 订单已支付或已取消,忽略操作(幂等处理)
System.out.println("订单" + orderNo + ":已支付/已取消,忽略取消操作");
}
}
}
2.3.2 测试验证
-
调用 /createOrder 接口,创建订单(状态为待支付),发送延迟消息;
-
场景1:30分钟内未支付 → 消费者接收延迟消息,取消订单、释放库存;
-
场景2:30分钟内调用 /payOrder 接口支付 → 订单状态变为已支付,消费者接收消息后忽略操作。
2.4 实战二:直播场景------开播5分钟发送优惠券消息
核心需求:主播开播后,延迟5分钟发送优惠券消息,推送所有在线用户,提升用户停留时长和转化率。
2.4.1 实战代码
步骤1:生产者开发(发送延迟消息)
开播接口调用后,发送延迟消息(延迟级别 3 → 10秒,可根据需求调整为级别 2 → 5秒),消息体为直播ID和优惠券信息。
bash
package com.example.liveservice.producer;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class LiveCouponProducer {
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 发送直播优惠券延迟消息(开播5分钟发送)
* @param liveId 直播ID
* @param couponInfo 优惠券信息(如"满100减20")
*/
public void sendLiveCouponDelayMessage(String liveId, String couponInfo) {
// 延迟级别 2 → 5秒(测试用),实际场景可改为对应5分钟的级别(默认无5分钟,需自定义配置)
// 自定义延迟级别:修改 broker.conf 增加 5m,对应级别 6(假设默认级别到5m)
int delayLevel = 6;
// 消息体:直播ID+优惠券信息(用逗号分隔)
String msgBody = liveId + "," + couponInfo;
rocketMQTemplate.syncSend(
"live-coupon-topic",
msgBody,
3000,
delayLevel
);
System.out.println("发送直播优惠券延迟消息成功,直播ID:" + liveId + ",延迟时间:5分钟");
}
}
步骤2:开播接口(关联延迟消息)
bash
package com.example.liveservice.controller;
import com.example.liveservice.producer.LiveCouponProducer;
import com.example.liveservice.service.LiveService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class LiveController {
@Autowired
private LiveService liveService;
@Autowired
private LiveCouponProducer liveCouponProducer;
// 开播接口
@PostMapping("/startLive")
public String startLive(@RequestParam String liveId, @RequestParam String anchorId) {
// 1. 开播逻辑(更新直播状态为"开播中")
boolean startResult = liveService.startLive(liveId, anchorId);
if (!startResult) {
return "开播失败";
}
// 2. 发送延迟消息(开播5分钟发送优惠券)
liveCouponProducer.sendLiveCouponDelayMessage(liveId, "满100减20,仅限直播期间使用");
return "开播成功,直播ID:" + liveId + ",5分钟后将发送优惠券";
}
}
步骤3:消费者开发(发送优惠券通知)
bash
package com.example.liveservice.consumer;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
@RocketMQMessageListener(
topic = "live-coupon-topic",
consumerGroup = "live-coupon-consumer-group"
)
public class LiveCouponConsumer implements RocketMQListener<String> {
@Autowired
private LiveService liveService;
@Override
public void onMessage(String msgBody) {
// 解析消息体:liveId,couponInfo
String[] msgArr = msgBody.split(",");
String liveId = msgArr[0];
String couponInfo = msgArr[1];
System.out.println("收到直播优惠券消息,直播ID:" + liveId + ",优惠券:" + couponInfo);
// 执行优惠券发送逻辑:推送所有在线用户
liveService.sendCouponToOnlineUser(liveId, couponInfo);
System.out.println("直播" + liveId + ":优惠券已推送所有在线用户");
}
}
2.5 延迟消息生产级优化与注意事项
2.5.1 优化建议
-
自定义延迟级别 :根据业务需求调整
messageDelayLevel,避免使用不必要的延迟级别,减少 Broker 扫描压力; -
消息去重:延迟消息可能因 Broker 异常导致重复投递,需实现消费幂等性(如通过订单状态、直播ID过滤);
-
延迟消息监控:通过 RocketMQ 控制台监控延迟消息的状态(未投递、已投递),及时发现异常;
-
高并发优化:大量延迟消息会增加 Broker 扫描压力,可增加 Broker 节点数量,优化延迟队列的存储路径(使用 SSD 磁盘)。
2.5.2 注意事项
-
延迟消息不支持事务消息:无法将延迟消息与分布式事务结合使用,若需事务+延迟,需手动实现(如先发送事务消息,消费后再发送延迟消息);
-
延迟时间精度:延迟消息的精度受 Broker 扫描频率影响,一般误差在 100ms 以内,不适合对时间精度要求极高的场景(如秒杀倒计时);
-
消息存储:延迟消息会先存入延迟队列,延迟时间到达后才投递到目标 Topic,需确保 Broker 磁盘空间充足,避免消息丢失。
三、死信队列深度应用:异常消息兜底,保障消息最终处理
3.1 核心痛点与死信队列定义
在消息投递和消费过程中,难免出现异常(如消费逻辑报错、网络异常、服务宕机),导致消息无法正常消费。若不处理这些异常消息,会导致:
-
消息堆积:异常消息一直留在队列中,占用资源,影响正常消息消费;
-
数据不一致:关键业务消息(如支付消息、订单消息)未被消费,导致业务流程中断;
-
异常排查困难:异常消息无法定位,难以排查问题根源。
RocketMQ 提供的死信队列(Dead-Letter Queue,DLQ),正是异常消息的"兜底容器"------当消息满足特定条件无法正常消费时,Broker 会将其自动转入死信队列,后续可通过人工干预或自动重试,确保消息最终被处理,避免消息丢失和数据不一致。
死信队列的核心定义
-
死信队列是特殊的 Topic ,命名规则为:
%DLQ%+消费者组名(如 %DLQ%logistics-consumer-group); -
每个消费者组对应一个死信队列,不同消费者组的死信队列相互独立;
-
死信队列中的消息不会自动被消费,需手动配置消费者订阅死信队列,进行后续处理;
-
死信队列中的消息保留时间与普通消息一致(由 broker.conf 中的 fileReservedTime 配置),可手动清理或持久化。
3.2 消息进入死信队列的3种场景
消息并非所有异常都会进入死信队列,只有满足以下3种条件之一,才会被转入死信队列:
-
消息消费失败,且重试次数达到上限:消费者消费消息时抛出异常,RocketMQ 会自动重试(默认重试 16 次),重试次数耗尽后,消息转入死信队列;
-
消息过期:消息设置了过期时间(TTL),超过过期时间未被消费,自动转入死信队列;
-
事务消息回查失败:分布式事务消息回查次数达到上限(默认 15 次),仍无法获取事务状态,消息转入死信队列。
关键说明 :
消息进入死信队列后,会保留原有的消息属性(如 Tag、自定义属性)和消息体,便于后续排查异常原因;同时,死信队列支持消息查询、重新投递,可实现异常消息的二次处理。
3.3 实战:死信队列处理异常消费消息(支付场景)
核心需求:支付消息消费时,因数据库异常导致消费失败,重试 16 次后仍失败,消息转入死信队列;后续通过订阅死信队列,人工排查异常后,重新投递消息,确保支付消息被正常处理。
3.3.1 实战代码
步骤1:普通消费者开发(处理支付消息,模拟异常)
模拟支付消息消费时,数据库异常导致消费失败,触发重试,重试次数耗尽后消息转入死信队列。
bash
package com.example.payservice.consumer;
import com.example.payservice.service.PayService;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
// 普通消费者:处理支付消息
@RocketMQMessageListener(
topic = "pay-topic",
consumerGroup = "pay-consumer-group",
maxReconsumeTimes = 3 // 简化测试,将重试次数改为3次(默认16次)
)
@Component
public class PayConsumer implements RocketMQListener<String> {
@Autowired
private PayService payService;
@Override
public void onMessage(String payNo) {
System.out.println("收到支付消息,支付编号:" + payNo);
try {
// 模拟数据库异常,导致消费失败
if (true) {
throw new RuntimeException("数据库连接异常,消费失败");
}
// 正常消费逻辑:处理支付回调、更新订单状态
payService.handlePayCallback(payNo);
System.out.println("支付消息消费成功,支付编号:" + payNo);
} catch (Exception e) {
e.printStackTrace();
// 消费失败,抛出异常,触发 RocketMQ 重试
throw new RuntimeException("支付消息消费失败,触发重试", e);
}
}
}
步骤2:死信队列消费者开发(处理异常消息)
订阅死信队列(%DLQ%pay-consumer-group),接收转入死信队列的异常消息,进行人工排查和二次处理。
bash
package com.example.payservice.consumer;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.apache.rocketmq.common.message.MessageExt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
// 死信队列消费者:订阅 pay-consumer-group 的死信队列
@RocketMQMessageListener(
topic = "%DLQ%pay-consumer-group", // 死信队列 Topic 命名规则
consumerGroup = "dlq-pay-consumer-group", // 死信消费者组(与普通消费者组区分)
maxReconsumeTimes = 1 // 死信消息重试1次,失败则人工处理
)
@Component
public class DlqPayConsumer implements RocketMQListener<MessageExt> {
@Autowired
private PayService payService;
@Override
public void onMessage(MessageExt message) {
String payNo = new String(message.getBody());
System.out.println("收到死信队列消息,支付编号:" + payNo + ",重试次数:" + message.getReconsumeTimes());
// 1. 记录死信消息日志,便于人工排查异常原因
recordDlqMessageLog(message);
try {
// 2. 人工排查后,尝试重新处理死信消息(模拟异常恢复,如数据库恢复连接)
payService.handlePayCallback(payNo);
System.out.println("死信消息处理成功,支付编号:" + payNo + ",已完成支付回调和订单状态更新");
} catch (Exception e) {
e.printStackTrace();
System.out.println("死信消息重试处理失败,支付编号:" + payNo + ",需人工介入处理");
// 重试1次失败后,不再重试,记录异常日志,通知运维人员人工处理
notifyOperationStaff(message);
}
}
/**
* 记录死信消息日志,包含消息属性、重试次数、异常原因等信息
* @param message 死信消息对象
*/
private void recordDlqMessageLog(MessageExt message) {
String payNo = new String(message.getBody());
int reconsumeTimes = message.getReconsumeTimes();
String msgId = message.getMsgId();
long bornTimestamp = message.getBornTimestamp();
// 实际业务中可写入日志系统(如ELK),便于后续排查
System.out.println("死信消息日志:msgId=" + msgId + ",payNo=" + payNo + ",重试次数=" + reconsumeTimes + ",产生时间=" + new java.util.Date(bornTimestamp));
}
/**
* 通知运维人员人工处理失败的死信消息
* @param message 死信消息对象
*/
private void notifyOperationStaff(MessageExt message) {
String payNo = new String(message.getBody());
// 实际业务中可通过短信、企业微信、邮件等方式通知运维人员
System.out.println("通知运维人员:死信消息处理失败,支付编号=" + payNo + ",请人工介入排查处理");
}
}
3.3.2 测试验证
通过模拟异常场景,验证死信队列的流转和处理逻辑,步骤如下:
-
调用支付消息生产者,发送支付消息(payNo=PAY20240401001);
-
普通消费者(PayConsumer)接收消息,模拟数据库异常,抛出异常触发重试;
-
重试3次(配置的maxReconsumeTimes=3)后仍失败,消息自动转入死信队列(%DLQ%pay-consumer-group);
-
死信消费者(DlqPayConsumer)订阅死信队列,接收死信消息并尝试重新处理;
-
场景1:模拟数据库恢复正常 → 死信消息处理成功,完成支付回调;
-
场景2:数据库仍异常 → 死信消息重试1次失败,记录日志并通知运维人员人工处理。
3.3.3 关键细节说明
-
死信消息属性获取:通过MessageExt对象可获取死信消息的详细属性,如msgId(消息唯一标识)、reconsumeTimes(重试次数)、bornTimestamp(消息产生时间)等,便于排查异常原因;
-
死信消费者配置:死信消费者组需与普通消费者组区分,避免冲突;maxReconsumeTimes建议设置为1,失败后直接人工处理,避免死信消息反复重试造成资源浪费;
-
人工介入机制:死信消息最终处理需结合人工排查,实际业务中可整合日志系统、告警系统,确保运维人员及时感知并处理异常。
3.4 死信队列生产级优化与最佳实践
3.4.1 优化建议
-
死信消息分类存储:针对不同业务类型的死信消息(如支付、订单、通知),可通过不同的消费者组区分,对应不同的死信队列,便于分类处理;
-
死信消息定时清理:结合业务需求,设置死信队列消息保留时间(通过broker.conf的fileReservedTime配置),定期清理已处理完成的死信消息,释放磁盘空间;
-
死信消息监控告警:通过RocketMQ控制台或自定义监控工具,监控死信队列的消息堆积情况,当堆积量超过阈值时,自动触发告警(如短信、企业微信通知);
-
死信消息自动重试机制:对于非致命异常(如网络波动),可在死信消费者中实现定时重试逻辑(如通过定时任务每隔10分钟重试一次),减少人工干预成本。
3.4.2 最佳实践
-
重试次数合理配置:普通消费者的maxReconsumeTimes建议根据业务场景调整,核心业务(如支付)可设置为3-5次,非核心业务(如通知)可设置为1-2次,避免无效重试;
-
死信消息与业务日志联动:将死信消息日志与业务日志关联,通过msgId可快速定位原消息的生产、投递记录,便于排查异常根源;
-
避免死信消息堆积:定期检查死信队列,及时处理未处理的死信消息,避免堆积过多导致磁盘空间不足,影响Broker正常运行;
-
死信消息二次投递:对于处理成功的死信消息,可记录处理状态,避免重复投递;对于处理失败的死信消息,可手动将其重新投递到原Topic,进行二次消费。
3.5 常见问题与解决方案
-
问题1:消息未进入死信队列排查方向:确认消费者是否抛出异常(未抛出异常不会触发重试)、maxReconsumeTimes是否配置正确、消息是否过期、事务消息回查是否正常;
-
解决方案:确保消费失败时抛出异常、正确配置maxReconsumeTimes、检查消息TTL设置、排查事务消息回查逻辑。
-
问题2:死信消费者无法接收死信消息排查方向:死信队列Topic命名是否正确(%DLQ%+消费者组名)、死信消费者组是否与普通消费者组区分、死信消费者的topic配置是否正确;
-
解决方案:核对死信队列Topic命名、修改死信消费者组名称、确保topic配置与死信队列一致。
-
问题3:死信消息处理后仍重复投递排查方向:死信消费者是否实现幂等处理、maxReconsumeTimes是否设置过大;
-
解决方案:通过支付编号、消息ID等唯一标识实现幂等处理、将maxReconsumeTimes设置为1。
四、总结:三大高级特性的场景适配与生产避坑指南
本篇文章围绕 RocketMQ 三大高级特性------消息过滤、延迟消息、死信队列,结合电商、直播、支付等典型业务场景,从核心原理、实战代码、生产优化三个维度,完成了深度解析与实战落地。三者的核心价值与场景适配总结如下:
-
消息过滤:解决"消息精细化路由"问题,Tag过滤适用于大多数简单场景,SQL过滤适用于复杂多条件场景,核心是"减少冗余、提升效率";
-
延迟消息:解决"分布式定时调度"问题,替代传统定时任务,依托Broker实现高可用定时投递,核心是"解耦、高效、低资源消耗";
-
死信队列:解决"异常消息兜底"问题,为无法正常消费的消息提供兜底容器,核心是"保障消息最终处理、避免数据不一致"。
生产级避坑核心要点:
-
优先使用Tag过滤,高并发场景避免过度使用SQL过滤,减少性能损耗;
-
延迟消息不支持自定义时间,需合理配置延迟级别,注意延迟精度和幂等处理;
-
死信队列需配置专门的消费者,建立完善的监控告警和人工处理机制,避免消息堆积;
-
三大特性均需结合业务场景合理选型,避免过度设计,确保代码简洁、可维护。
下一篇,我们将深入 RocketMQ 消息追踪与性能调优进阶,拆解消息全链路追踪实现方案、高并发场景性能调优细节,以及集群容灾策略,延续"原理+实战+避坑"风格,进一步完善 RocketMQ 技术体系。