RocketMQ 系列文章(高级篇第 4 篇):消息过滤、延迟消息与死信队列深度应用实战

前言:从 "基础投递" 到 "场景适配",解锁 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);

  1. SQL 过滤性能低于 Tag 过滤 ,高并发场景下优先使用 Tag 过滤;

3.不支持复杂的函数调用(如 SUBSTR(age, 1, 2)),仅支持基础运算;

4.需确保 Broker 开启 enablePropertyFilter = true,否则 SQL 过滤无效。

二、延迟消息实战:实现业务定时调度,适配直播 / 预约场景

2.1 核心痛点与应用场景

在分布式业务中,经常需要延迟执行某些操作,例如:

  • 直播场景:开播后 5 分钟自动发送优惠券消息,提升用户停留时长;
  • 电商场景:订单创建后 30 分钟未支付,自动取消订单并释放库存;
  • 预约场景:用户预约活动开始前 1 小时,自动发送提醒消息。

传统解决方案是定时任务(如 Quartz、XXL-Job),但存在两个核心问题:

  1. 时效性差:定时任务通常按分钟/小时调度,无法实现毫秒级延迟;

  2. 资源浪费:大量定时任务占用数据库或调度服务资源,高并发场景下易出现调度瓶颈;

  3. 耦合度高:定时任务与业务逻辑绑定,代码维护成本高,且不易扩展。

RocketMQ 延迟消息完美解决了这些问题,它允许生产者发送消息时指定延迟时间,Broker 会在延迟时间到达后,自动将消息投递到消费者,实现"定时调度"功能,且依托 RocketMQ 集群的高可用性,无需额外搭建调度服务,性能更优、耦合度更低。

2.2 延迟消息核心原理

RocketMQ 延迟消息的核心逻辑的是"延迟队列+定时投递",具体流程如下:

  1. 生产者发送延迟消息时,指定延迟级别(而非具体延迟时间);

  2. Broker 接收消息后,不会立即将其存入目标 Topic 的队列,而是存入内置延迟队列(SCHEDULE_TOPIC_XXXX);

  3. Broker 内部有定时任务,定期扫描延迟队列,判断消息延迟时间是否到达;

  4. 若延迟时间到达,Broker 将消息从延迟队列中取出,投递到目标 Topic 的队列,供消费者消费;

  5. 若延迟时间未到达,继续存放在延迟队列中,等待下一次扫描。

关键说明:延迟级别

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. 延迟级别从 1 开始计数,不可跳过级别(如不能直接指定级别 0 或级别 19);

  2. 延迟消息的延迟时间是"消息发送到 Broker 后开始计算",而非"消费者接收消息后开始计算";

  3. 延迟消息不支持重试机制(若消费失败,会进入死信队列),需提前做好异常处理。

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 测试验证

  1. 调用 /createOrder 接口,创建订单(状态为待支付),发送延迟消息;

  2. 场景1:30分钟内未支付 → 消费者接收延迟消息,取消订单、释放库存;

  3. 场景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 优化建议

  1. 自定义延迟级别 :根据业务需求调整 messageDelayLevel,避免使用不必要的延迟级别,减少 Broker 扫描压力;

  2. 消息去重:延迟消息可能因 Broker 异常导致重复投递,需实现消费幂等性(如通过订单状态、直播ID过滤);

  3. 延迟消息监控:通过 RocketMQ 控制台监控延迟消息的状态(未投递、已投递),及时发现异常;

  4. 高并发优化:大量延迟消息会增加 Broker 扫描压力,可增加 Broker 节点数量,优化延迟队列的存储路径(使用 SSD 磁盘)。

2.5.2 注意事项

  1. 延迟消息不支持事务消息:无法将延迟消息与分布式事务结合使用,若需事务+延迟,需手动实现(如先发送事务消息,消费后再发送延迟消息);

  2. 延迟时间精度:延迟消息的精度受 Broker 扫描频率影响,一般误差在 100ms 以内,不适合对时间精度要求极高的场景(如秒杀倒计时);

  3. 消息存储:延迟消息会先存入延迟队列,延迟时间到达后才投递到目标 Topic,需确保 Broker 磁盘空间充足,避免消息丢失。

三、死信队列深度应用:异常消息兜底,保障消息最终处理

3.1 核心痛点与死信队列定义

在消息投递和消费过程中,难免出现异常(如消费逻辑报错、网络异常、服务宕机),导致消息无法正常消费。若不处理这些异常消息,会导致:

  1. 消息堆积:异常消息一直留在队列中,占用资源,影响正常消息消费;

  2. 数据不一致:关键业务消息(如支付消息、订单消息)未被消费,导致业务流程中断;

  3. 异常排查困难:异常消息无法定位,难以排查问题根源。

RocketMQ 提供的死信队列(Dead-Letter Queue,DLQ),正是异常消息的"兜底容器"------当消息满足特定条件无法正常消费时,Broker 会将其自动转入死信队列,后续可通过人工干预或自动重试,确保消息最终被处理,避免消息丢失和数据不一致。

死信队列的核心定义

  • 死信队列是特殊的 Topic ,命名规则为:%DLQ%+消费者组名(如 %DLQ%logistics-consumer-group);

  • 每个消费者组对应一个死信队列,不同消费者组的死信队列相互独立;

  • 死信队列中的消息不会自动被消费,需手动配置消费者订阅死信队列,进行后续处理;

  • 死信队列中的消息保留时间与普通消息一致(由 broker.conf 中的 fileReservedTime 配置),可手动清理或持久化。

3.2 消息进入死信队列的3种场景

消息并非所有异常都会进入死信队列,只有满足以下3种条件之一,才会被转入死信队列:

  1. 消息消费失败,且重试次数达到上限:消费者消费消息时抛出异常,RocketMQ 会自动重试(默认重试 16 次),重试次数耗尽后,消息转入死信队列;

  2. 消息过期:消息设置了过期时间(TTL),超过过期时间未被消费,自动转入死信队列;

  3. 事务消息回查失败:分布式事务消息回查次数达到上限(默认 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 测试验证

通过模拟异常场景,验证死信队列的流转和处理逻辑,步骤如下:

  1. 调用支付消息生产者,发送支付消息(payNo=PAY20240401001);

  2. 普通消费者(PayConsumer)接收消息,模拟数据库异常,抛出异常触发重试;

  3. 重试3次(配置的maxReconsumeTimes=3)后仍失败,消息自动转入死信队列(%DLQ%pay-consumer-group);

  4. 死信消费者(DlqPayConsumer)订阅死信队列,接收死信消息并尝试重新处理;

  5. 场景1:模拟数据库恢复正常 → 死信消息处理成功,完成支付回调;

  6. 场景2:数据库仍异常 → 死信消息重试1次失败,记录日志并通知运维人员人工处理。

3.3.3 关键细节说明

  1. 死信消息属性获取:通过MessageExt对象可获取死信消息的详细属性,如msgId(消息唯一标识)、reconsumeTimes(重试次数)、bornTimestamp(消息产生时间)等,便于排查异常原因;

  2. 死信消费者配置:死信消费者组需与普通消费者组区分,避免冲突;maxReconsumeTimes建议设置为1,失败后直接人工处理,避免死信消息反复重试造成资源浪费;

  3. 人工介入机制:死信消息最终处理需结合人工排查,实际业务中可整合日志系统、告警系统,确保运维人员及时感知并处理异常。

3.4 死信队列生产级优化与最佳实践
3.4.1 优化建议

  1. 死信消息分类存储:针对不同业务类型的死信消息(如支付、订单、通知),可通过不同的消费者组区分,对应不同的死信队列,便于分类处理;

  2. 死信消息定时清理:结合业务需求,设置死信队列消息保留时间(通过broker.conf的fileReservedTime配置),定期清理已处理完成的死信消息,释放磁盘空间;

  3. 死信消息监控告警:通过RocketMQ控制台或自定义监控工具,监控死信队列的消息堆积情况,当堆积量超过阈值时,自动触发告警(如短信、企业微信通知);

  4. 死信消息自动重试机制:对于非致命异常(如网络波动),可在死信消费者中实现定时重试逻辑(如通过定时任务每隔10分钟重试一次),减少人工干预成本。

3.4.2 最佳实践

  1. 重试次数合理配置:普通消费者的maxReconsumeTimes建议根据业务场景调整,核心业务(如支付)可设置为3-5次,非核心业务(如通知)可设置为1-2次,避免无效重试;

  2. 死信消息与业务日志联动:将死信消息日志与业务日志关联,通过msgId可快速定位原消息的生产、投递记录,便于排查异常根源;

  3. 避免死信消息堆积:定期检查死信队列,及时处理未处理的死信消息,避免堆积过多导致磁盘空间不足,影响Broker正常运行;

  4. 死信消息二次投递:对于处理成功的死信消息,可记录处理状态,避免重复投递;对于处理失败的死信消息,可手动将其重新投递到原Topic,进行二次消费。

3.5 常见问题与解决方案

  1. 问题1:消息未进入死信队列排查方向:确认消费者是否抛出异常(未抛出异常不会触发重试)、maxReconsumeTimes是否配置正确、消息是否过期、事务消息回查是否正常;

  2. 解决方案:确保消费失败时抛出异常、正确配置maxReconsumeTimes、检查消息TTL设置、排查事务消息回查逻辑。

  3. 问题2:死信消费者无法接收死信消息排查方向:死信队列Topic命名是否正确(%DLQ%+消费者组名)、死信消费者组是否与普通消费者组区分、死信消费者的topic配置是否正确;

  4. 解决方案:核对死信队列Topic命名、修改死信消费者组名称、确保topic配置与死信队列一致。

  5. 问题3:死信消息处理后仍重复投递排查方向:死信消费者是否实现幂等处理、maxReconsumeTimes是否设置过大;

  6. 解决方案:通过支付编号、消息ID等唯一标识实现幂等处理、将maxReconsumeTimes设置为1。

四、总结:三大高级特性的场景适配与生产避坑指南

本篇文章围绕 RocketMQ 三大高级特性------消息过滤、延迟消息、死信队列,结合电商、直播、支付等典型业务场景,从核心原理、实战代码、生产优化三个维度,完成了深度解析与实战落地。三者的核心价值与场景适配总结如下:

  1. 消息过滤:解决"消息精细化路由"问题,Tag过滤适用于大多数简单场景,SQL过滤适用于复杂多条件场景,核心是"减少冗余、提升效率";

  2. 延迟消息:解决"分布式定时调度"问题,替代传统定时任务,依托Broker实现高可用定时投递,核心是"解耦、高效、低资源消耗";

  3. 死信队列:解决"异常消息兜底"问题,为无法正常消费的消息提供兜底容器,核心是"保障消息最终处理、避免数据不一致"。

生产级避坑核心要点:

  • 优先使用Tag过滤,高并发场景避免过度使用SQL过滤,减少性能损耗;

  • 延迟消息不支持自定义时间,需合理配置延迟级别,注意延迟精度和幂等处理;

  • 死信队列需配置专门的消费者,建立完善的监控告警和人工处理机制,避免消息堆积;

  • 三大特性均需结合业务场景合理选型,避免过度设计,确保代码简洁、可维护。

下一篇,我们将深入 RocketMQ 消息追踪与性能调优进阶,拆解消息全链路追踪实现方案、高并发场景性能调优细节,以及集群容灾策略,延续"原理+实战+避坑"风格,进一步完善 RocketMQ 技术体系。

相关推荐
开发者联盟league2 天前
在windows上安装和运行rocketmq
windows·rocketmq
冷小鱼2 天前
消息队列(MQ)技术全景科普:从选型到AI+未来
人工智能·kafka·rabbitmq·rocketmq·mq·pulsar
筠·3 天前
Docker Compose 部署 RocketMQ
docker·rocketmq·java-rocketmq
Rcnhtin7 天前
RocketMQ
java·linux·rocketmq
qq_297574677 天前
RocketMQ 系列文章(高级篇第 1 篇):高可用集群部署与运维监控实战指南
运维·rocketmq·java-rocketmq
成为大佬先秃头8 天前
解决RocketMQ-Dashboard开启登录认证后不生效
rocketmq
卷毛的技术笔记9 天前
从“拆东墙补西墙”到“最终一致”:分布式事务在Spring Boot/Cloud中的破局之道
java·spring boot·分布式·后端·spring cloud·面试·rocketmq
qq_297574679 天前
RocketMQ 系列文章(进阶篇第 4 篇):死信队列与延迟消息实战指南
rocketmq
KAI丶9 天前
【RocketMQ】dashboard消息展示重复
rocketmq