RocketMQ学习-Spring Boot消息实践

RocketMQ

内容发布、点赞、关注等高频操作,往往还伴随着统计更新、数据库落库和缓存失效。如果把这些工作全部放在接口线程中同步执行,响应时间会随着下游操作不断增加,任意一个非核心步骤失败也可能影响主流程。

RocketMQ 将主流程与后续处理拆开:生产者只负责发布业务事件,消费者按照自己的处理能力完成落库、计数或缓存清理。消息在两者之间提供缓冲,使系统获得异步处理、业务解耦和流量削峰能力。

本文先建立最基本的发送与消费链路,再扩展到实际业务中常见的 Tag、顺序消息、广播消息和延迟消息。

消息流转与基本角色

#mermaid-svg-laoxw0oSTJKUGYmC{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-laoxw0oSTJKUGYmC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-laoxw0oSTJKUGYmC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-laoxw0oSTJKUGYmC .error-icon{fill:#552222;}#mermaid-svg-laoxw0oSTJKUGYmC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-laoxw0oSTJKUGYmC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-laoxw0oSTJKUGYmC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-laoxw0oSTJKUGYmC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-laoxw0oSTJKUGYmC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-laoxw0oSTJKUGYmC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-laoxw0oSTJKUGYmC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-laoxw0oSTJKUGYmC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-laoxw0oSTJKUGYmC .marker.cross{stroke:#333333;}#mermaid-svg-laoxw0oSTJKUGYmC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-laoxw0oSTJKUGYmC p{margin:0;}#mermaid-svg-laoxw0oSTJKUGYmC .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-laoxw0oSTJKUGYmC .cluster-label text{fill:#333;}#mermaid-svg-laoxw0oSTJKUGYmC .cluster-label span{color:#333;}#mermaid-svg-laoxw0oSTJKUGYmC .cluster-label span p{background-color:transparent;}#mermaid-svg-laoxw0oSTJKUGYmC .label text,#mermaid-svg-laoxw0oSTJKUGYmC span{fill:#333;color:#333;}#mermaid-svg-laoxw0oSTJKUGYmC .node rect,#mermaid-svg-laoxw0oSTJKUGYmC .node circle,#mermaid-svg-laoxw0oSTJKUGYmC .node ellipse,#mermaid-svg-laoxw0oSTJKUGYmC .node polygon,#mermaid-svg-laoxw0oSTJKUGYmC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-laoxw0oSTJKUGYmC .rough-node .label text,#mermaid-svg-laoxw0oSTJKUGYmC .node .label text,#mermaid-svg-laoxw0oSTJKUGYmC .image-shape .label,#mermaid-svg-laoxw0oSTJKUGYmC .icon-shape .label{text-anchor:middle;}#mermaid-svg-laoxw0oSTJKUGYmC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-laoxw0oSTJKUGYmC .rough-node .label,#mermaid-svg-laoxw0oSTJKUGYmC .node .label,#mermaid-svg-laoxw0oSTJKUGYmC .image-shape .label,#mermaid-svg-laoxw0oSTJKUGYmC .icon-shape .label{text-align:center;}#mermaid-svg-laoxw0oSTJKUGYmC .node.clickable{cursor:pointer;}#mermaid-svg-laoxw0oSTJKUGYmC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-laoxw0oSTJKUGYmC .arrowheadPath{fill:#333333;}#mermaid-svg-laoxw0oSTJKUGYmC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-laoxw0oSTJKUGYmC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-laoxw0oSTJKUGYmC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-laoxw0oSTJKUGYmC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-laoxw0oSTJKUGYmC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-laoxw0oSTJKUGYmC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-laoxw0oSTJKUGYmC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-laoxw0oSTJKUGYmC .cluster text{fill:#333;}#mermaid-svg-laoxw0oSTJKUGYmC .cluster span{color:#333;}#mermaid-svg-laoxw0oSTJKUGYmC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-laoxw0oSTJKUGYmC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-laoxw0oSTJKUGYmC rect.text{fill:none;stroke-width:0;}#mermaid-svg-laoxw0oSTJKUGYmC .icon-shape,#mermaid-svg-laoxw0oSTJKUGYmC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-laoxw0oSTJKUGYmC .icon-shape p,#mermaid-svg-laoxw0oSTJKUGYmC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-laoxw0oSTJKUGYmC .icon-shape .label rect,#mermaid-svg-laoxw0oSTJKUGYmC .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-laoxw0oSTJKUGYmC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-laoxw0oSTJKUGYmC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-laoxw0oSTJKUGYmC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 生产者
RocketMQ
消费者

  • 生产者:发送消息的一方;
  • RocketMQ:接收并保存消息;
  • 消费者:收到消息后执行业务代码。

以点赞异步落库为例:

text 复制代码
用户点赞
-> 生产者发送"点赞消息"
-> RocketMQ 保存消息
-> 消费者收到消息
-> 消费者把点赞记录写入数据库

接口只需要快速处理 Redis 和发送消息,数据库落库可以由消费者异步完成。


引入 Spring Boot Starter

xml 复制代码
<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-spring-boot-starter</artifactId>
    <version>2.3.6</version>
</dependency>

引入依赖后,Spring Boot 会自动创建发送消息需要的 RocketMQTemplate


基础配置

yaml 复制代码
rocketmq:
  # RocketMQ NameServer 地址
  name-server: 127.0.0.1:9876

  producer:
    # 生产者组名称,不能省略
    group: demo-producer-group

name-server

yaml 复制代码
name-server: 127.0.0.1:9876

指定 NameServer 地址。客户端通过 NameServer 获取 Topic 对应的 Broker 路由。

producer.group

yaml 复制代码
producer:
  group: demo-producer-group

指定 Producer Group,用来标识一类具有相同发送职责的生产者。


普通消息发送

创建一个生产者:

java 复制代码
@Component
public class DemoProducer {

    @Resource
    private RocketMQTemplate rocketMQTemplate;

    public void send() {
        rocketMQTemplate.syncSend(
                "demo-topic",
                "hello RocketMQ"
        );
    }
}

syncSend 接收发送地址和消息内容:

java 复制代码
rocketMQTemplate.syncSend(
        "demo-topic",       // 消息发送到哪里
        "hello RocketMQ"    // 消息内容
);

demo-topic 是 Topic,可以理解成消息分类。

text 复制代码
点赞消息放到 like-topic
内容操作消息放到 content-topic
缓存删除消息放到 cache-delete-topic

syncSend 表示同步发送:当前线程会等待 RocketMQ 返回发送结果。


普通消息消费

java 复制代码
@Component
@RocketMQMessageListener(
        consumerGroup = "demo-consumer-group",
        topic = "demo-topic"
)
public class DemoConsumer implements RocketMQListener<String> {

    @Override
    public void onMessage(String message) {
        System.out.println("收到消息:" + message);
    }
}

@RocketMQMessageListener 声明 Consumer Group 和订阅的 Topic。RocketMQListener<String> 表示消息体按照字符串转换,消息到达后由 onMessage 执行业务逻辑。

完整过程:
DemoConsumer RocketMQ DemoProducer DemoConsumer RocketMQ DemoProducer #mermaid-svg-6UaCbM8RezGGUyDk{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6UaCbM8RezGGUyDk .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6UaCbM8RezGGUyDk .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6UaCbM8RezGGUyDk .error-icon{fill:#552222;}#mermaid-svg-6UaCbM8RezGGUyDk .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6UaCbM8RezGGUyDk .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6UaCbM8RezGGUyDk .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6UaCbM8RezGGUyDk .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6UaCbM8RezGGUyDk .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6UaCbM8RezGGUyDk .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6UaCbM8RezGGUyDk .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6UaCbM8RezGGUyDk .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6UaCbM8RezGGUyDk .marker.cross{stroke:#333333;}#mermaid-svg-6UaCbM8RezGGUyDk svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6UaCbM8RezGGUyDk p{margin:0;}#mermaid-svg-6UaCbM8RezGGUyDk .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-6UaCbM8RezGGUyDk text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-6UaCbM8RezGGUyDk .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-6UaCbM8RezGGUyDk .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-6UaCbM8RezGGUyDk .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-6UaCbM8RezGGUyDk .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-6UaCbM8RezGGUyDk #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-6UaCbM8RezGGUyDk .sequenceNumber{fill:white;}#mermaid-svg-6UaCbM8RezGGUyDk #sequencenumber{fill:#333;}#mermaid-svg-6UaCbM8RezGGUyDk #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-6UaCbM8RezGGUyDk .messageText{fill:#333;stroke:none;}#mermaid-svg-6UaCbM8RezGGUyDk .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-6UaCbM8RezGGUyDk .labelText,#mermaid-svg-6UaCbM8RezGGUyDk .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-6UaCbM8RezGGUyDk .loopText,#mermaid-svg-6UaCbM8RezGGUyDk .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-6UaCbM8RezGGUyDk .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-6UaCbM8RezGGUyDk .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-6UaCbM8RezGGUyDk .noteText,#mermaid-svg-6UaCbM8RezGGUyDk .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-6UaCbM8RezGGUyDk .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-6UaCbM8RezGGUyDk .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-6UaCbM8RezGGUyDk .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-6UaCbM8RezGGUyDk .actorPopupMenu{position:absolute;}#mermaid-svg-6UaCbM8RezGGUyDk .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-6UaCbM8RezGGUyDk .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-6UaCbM8RezGGUyDk .actor-man circle,#mermaid-svg-6UaCbM8RezGGUyDk line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-6UaCbM8RezGGUyDk :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} demo-topic / hello RocketMQ推送消息打印"收到消息"

这构成了 RocketMQ 最基本的消息链路,后续消息模型都建立在相同的发送与监听机制上。


对象消息

真实业务不会只发送一句字符串,通常会发送一个对象。

例如点赞消息:

java 复制代码
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LikeEvent {
    private Long userId;
    private Long contentId;
    private LocalDateTime createTime;
}

发送对象:

java 复制代码
LikeEvent event = LikeEvent.builder()
        .userId(userId)
        .contentId(contentId)
        .createTime(LocalDateTime.now())
        .build();

rocketMQTemplate.convertAndSend(
        "like-topic",
        event
);

消费者直接使用 LikeEvent 接收:

java 复制代码
@Component
@RocketMQMessageListener(
        consumerGroup = "like-consumer-group",
        topic = "like-topic"
)
public class LikeConsumer implements RocketMQListener<LikeEvent> {

    @Override
    public void onMessage(LikeEvent event) {
        System.out.println("用户:" + event.getUserId());
        System.out.println("内容:" + event.getContentId());

        // 将点赞记录写入数据库
        saveLike(event);
    }
}

这时消息流程变成:

text 复制代码
接口构建 LikeEvent
-> RocketMQTemplate 发送
-> RocketMQ 保存
-> LikeConsumer 接收
-> 写入数据库

同步发送与异步发送

同步发送

java 复制代码
SendResult result = rocketMQTemplate.syncSend(
        "like-topic",
        event
);

同步发送会等待 RocketMQ 返回结果。

text 复制代码
发送消息
-> 等待 RocketMQ 响应
-> 得到 SendResult
-> 继续执行

适合必须确认发送结果的消息。

异步发送

java 复制代码
rocketMQTemplate.asyncSend(
        "like-topic",
        event,
        new SendCallback() {
            @Override
            public void onSuccess(SendResult result) {
                System.out.println("发送成功:" + result.getMsgId());
            }

            @Override
            public void onException(Throwable ex) {
                System.out.println("发送失败:" + ex.getMessage());
            }
        }
);

异步发送不会一直等待结果。RocketMQ 响应后,会执行回调:

text 复制代码
发送成功 -> onSuccess
发送失败 -> onException

点赞、发布后统计等对接口响应速度要求较高的操作,可以使用异步发送。

不能只写 onSuccess。如果 onException 什么都不做,消息发送失败后就丢失了,至少要记录日志和告警。


Tag 过滤与操作分类

点赞和取消点赞属于同一类消息,可以放在同一个 Topic,通过 Tag 区分。

text 复制代码
LikeOperateTopic:Like
LikeOperateTopic:Unlike

格式是:

text 复制代码
Topic:Tag

发送点赞消息

java 复制代码
rocketMQTemplate.convertAndSend(
        "LikeOperateTopic:Like",
        event
);

发送取消点赞消息

java 复制代码
rocketMQTemplate.convertAndSend(
        "LikeOperateTopic:Unlike",
        event
);

在同一个消费者中判断 Tag

点赞和取消点赞需要保持先后顺序,所以使用同一个消费者组、同一个消费者处理,再根据 Tag 选择业务逻辑:

java 复制代码
@Component
@RocketMQMessageListener(
        consumerGroup = "like-operate-consumer-group",
        topic = "LikeOperateTopic",
        selectorExpression = "Like || Unlike"
)
public class LikeOperateConsumer
        implements RocketMQListener<Message> {

    @Override
    public void onMessage(Message message) {
        String body = new String(
                message.getBody(),
                StandardCharsets.UTF_8
        );

        String tag = message.getTags();
        LikeEvent event = fromJson(body, LikeEvent.class);

        if ("Like".equals(tag)) {
            saveLike(event);
        } else if ("Unlike".equals(tag)) {
            deleteLike(event);
        }
    }
}

这里消费的是 RocketMQ 原始 Message,因为需要通过 message.getTags() 读取 Tag。

三者的职责可以归纳为:

text 复制代码
Topic:大分类
Tag:分类下面的具体操作
Consumer Group:哪一组消费者负责处理

顺序消息

同一个用户可能连续执行:

text 复制代码
点赞 -> 取消点赞 -> 再次点赞

如果三条消息乱序消费,数据库中的最终状态可能不正确。

顺序消息需要完成两件事。

生产者使用相同 hashKey

java 复制代码
String hashKey = String.valueOf(userId);

rocketMQTemplate.asyncSendOrderly(
        "LikeOperateTopic:Like",
        event,
        hashKey,
        new SendCallback() {
            @Override
            public void onSuccess(SendResult result) {
                System.out.println("发送成功");
            }

            @Override
            public void onException(Throwable ex) {
                System.out.println("发送失败");
            }
        }
);

相同 hashKey 的消息会被发送到同一个队列。

点赞和取消点赞都使用同一个 userId 作为 hashKey

text 复制代码
用户 1001 点赞       -> hashKey = 1001
用户 1001 取消点赞   -> hashKey = 1001
用户 1001 再次点赞   -> hashKey = 1001

消费者开启顺序消费

java 复制代码
@RocketMQMessageListener(
        consumerGroup = "like-operate-consumer-group",
        topic = "LikeOperateTopic",
        selectorExpression = "Like || Unlike",
        consumeMode = ConsumeMode.ORDERLY
)

也就是给前面的 LikeOperateConsumer 增加 consumeMode = ConsumeMode.ORDERLY

生产者和消费者必须同时配置:

text 复制代码
生产者:asyncSendOrderly + 相同 hashKey
消费者:consumeMode = ORDERLY

RocketMQ 只能保证同一个队列内有序,不能保证整个 Topic 中所有消息全局有序。所以通过 hashKey 来保证放到同一个队列里面


广播消息与本地缓存失效

普通消费默认使用集群模式。

假设有三个实例属于同一个消费者组:

text 复制代码
一条消息只会被其中一个实例消费

但是删除 Caffeine 本地缓存时,每个实例都有自己的缓存:
#mermaid-svg-j1VfU95VfX3IFcCe{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-j1VfU95VfX3IFcCe .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-j1VfU95VfX3IFcCe .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-j1VfU95VfX3IFcCe .error-icon{fill:#552222;}#mermaid-svg-j1VfU95VfX3IFcCe .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-j1VfU95VfX3IFcCe .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-j1VfU95VfX3IFcCe .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-j1VfU95VfX3IFcCe .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-j1VfU95VfX3IFcCe .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-j1VfU95VfX3IFcCe .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-j1VfU95VfX3IFcCe .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-j1VfU95VfX3IFcCe .marker{fill:#333333;stroke:#333333;}#mermaid-svg-j1VfU95VfX3IFcCe .marker.cross{stroke:#333333;}#mermaid-svg-j1VfU95VfX3IFcCe svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-j1VfU95VfX3IFcCe p{margin:0;}#mermaid-svg-j1VfU95VfX3IFcCe .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-j1VfU95VfX3IFcCe .cluster-label text{fill:#333;}#mermaid-svg-j1VfU95VfX3IFcCe .cluster-label span{color:#333;}#mermaid-svg-j1VfU95VfX3IFcCe .cluster-label span p{background-color:transparent;}#mermaid-svg-j1VfU95VfX3IFcCe .label text,#mermaid-svg-j1VfU95VfX3IFcCe span{fill:#333;color:#333;}#mermaid-svg-j1VfU95VfX3IFcCe .node rect,#mermaid-svg-j1VfU95VfX3IFcCe .node circle,#mermaid-svg-j1VfU95VfX3IFcCe .node ellipse,#mermaid-svg-j1VfU95VfX3IFcCe .node polygon,#mermaid-svg-j1VfU95VfX3IFcCe .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-j1VfU95VfX3IFcCe .rough-node .label text,#mermaid-svg-j1VfU95VfX3IFcCe .node .label text,#mermaid-svg-j1VfU95VfX3IFcCe .image-shape .label,#mermaid-svg-j1VfU95VfX3IFcCe .icon-shape .label{text-anchor:middle;}#mermaid-svg-j1VfU95VfX3IFcCe .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-j1VfU95VfX3IFcCe .rough-node .label,#mermaid-svg-j1VfU95VfX3IFcCe .node .label,#mermaid-svg-j1VfU95VfX3IFcCe .image-shape .label,#mermaid-svg-j1VfU95VfX3IFcCe .icon-shape .label{text-align:center;}#mermaid-svg-j1VfU95VfX3IFcCe .node.clickable{cursor:pointer;}#mermaid-svg-j1VfU95VfX3IFcCe .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-j1VfU95VfX3IFcCe .arrowheadPath{fill:#333333;}#mermaid-svg-j1VfU95VfX3IFcCe .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-j1VfU95VfX3IFcCe .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-j1VfU95VfX3IFcCe .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-j1VfU95VfX3IFcCe .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-j1VfU95VfX3IFcCe .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-j1VfU95VfX3IFcCe .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-j1VfU95VfX3IFcCe .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-j1VfU95VfX3IFcCe .cluster text{fill:#333;}#mermaid-svg-j1VfU95VfX3IFcCe .cluster span{color:#333;}#mermaid-svg-j1VfU95VfX3IFcCe div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-j1VfU95VfX3IFcCe .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-j1VfU95VfX3IFcCe rect.text{fill:none;stroke-width:0;}#mermaid-svg-j1VfU95VfX3IFcCe .icon-shape,#mermaid-svg-j1VfU95VfX3IFcCe .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-j1VfU95VfX3IFcCe .icon-shape p,#mermaid-svg-j1VfU95VfX3IFcCe .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-j1VfU95VfX3IFcCe .icon-shape .label rect,#mermaid-svg-j1VfU95VfX3IFcCe .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-j1VfU95VfX3IFcCe .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-j1VfU95VfX3IFcCe .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-j1VfU95VfX3IFcCe :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 实例 A
Caffeine A
实例 B
Caffeine B
实例 C
Caffeine C

所以必须让三个实例都收到消息。

发送缓存删除消息

java 复制代码
rocketMQTemplate.syncSend(
        "LocalCacheDeleteTopic",
        contentId
);

消费者开启广播模式

java 复制代码
@Component
@RocketMQMessageListener(
        consumerGroup = "local-cache-delete-group",
        topic = "LocalCacheDeleteTopic",
        messageModel = MessageModel.BROADCASTING
)
public class LocalCacheDeleteConsumer
        implements RocketMQListener<Long> {

    @Override
    public void onMessage(Long contentId) {
        localCache.invalidate(contentId);
    }
}

两种模式的区别:
#mermaid-svg-T1NPRv31F245J4I6{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-T1NPRv31F245J4I6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-T1NPRv31F245J4I6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-T1NPRv31F245J4I6 .error-icon{fill:#552222;}#mermaid-svg-T1NPRv31F245J4I6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-T1NPRv31F245J4I6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-T1NPRv31F245J4I6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-T1NPRv31F245J4I6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-T1NPRv31F245J4I6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-T1NPRv31F245J4I6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-T1NPRv31F245J4I6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-T1NPRv31F245J4I6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-T1NPRv31F245J4I6 .marker.cross{stroke:#333333;}#mermaid-svg-T1NPRv31F245J4I6 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-T1NPRv31F245J4I6 p{margin:0;}#mermaid-svg-T1NPRv31F245J4I6 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-T1NPRv31F245J4I6 .cluster-label text{fill:#333;}#mermaid-svg-T1NPRv31F245J4I6 .cluster-label span{color:#333;}#mermaid-svg-T1NPRv31F245J4I6 .cluster-label span p{background-color:transparent;}#mermaid-svg-T1NPRv31F245J4I6 .label text,#mermaid-svg-T1NPRv31F245J4I6 span{fill:#333;color:#333;}#mermaid-svg-T1NPRv31F245J4I6 .node rect,#mermaid-svg-T1NPRv31F245J4I6 .node circle,#mermaid-svg-T1NPRv31F245J4I6 .node ellipse,#mermaid-svg-T1NPRv31F245J4I6 .node polygon,#mermaid-svg-T1NPRv31F245J4I6 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-T1NPRv31F245J4I6 .rough-node .label text,#mermaid-svg-T1NPRv31F245J4I6 .node .label text,#mermaid-svg-T1NPRv31F245J4I6 .image-shape .label,#mermaid-svg-T1NPRv31F245J4I6 .icon-shape .label{text-anchor:middle;}#mermaid-svg-T1NPRv31F245J4I6 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-T1NPRv31F245J4I6 .rough-node .label,#mermaid-svg-T1NPRv31F245J4I6 .node .label,#mermaid-svg-T1NPRv31F245J4I6 .image-shape .label,#mermaid-svg-T1NPRv31F245J4I6 .icon-shape .label{text-align:center;}#mermaid-svg-T1NPRv31F245J4I6 .node.clickable{cursor:pointer;}#mermaid-svg-T1NPRv31F245J4I6 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-T1NPRv31F245J4I6 .arrowheadPath{fill:#333333;}#mermaid-svg-T1NPRv31F245J4I6 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-T1NPRv31F245J4I6 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-T1NPRv31F245J4I6 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-T1NPRv31F245J4I6 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-T1NPRv31F245J4I6 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-T1NPRv31F245J4I6 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-T1NPRv31F245J4I6 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-T1NPRv31F245J4I6 .cluster text{fill:#333;}#mermaid-svg-T1NPRv31F245J4I6 .cluster span{color:#333;}#mermaid-svg-T1NPRv31F245J4I6 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-T1NPRv31F245J4I6 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-T1NPRv31F245J4I6 rect.text{fill:none;stroke-width:0;}#mermaid-svg-T1NPRv31F245J4I6 .icon-shape,#mermaid-svg-T1NPRv31F245J4I6 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-T1NPRv31F245J4I6 .icon-shape p,#mermaid-svg-T1NPRv31F245J4I6 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-T1NPRv31F245J4I6 .icon-shape .label rect,#mermaid-svg-T1NPRv31F245J4I6 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-T1NPRv31F245J4I6 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-T1NPRv31F245J4I6 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-T1NPRv31F245J4I6 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 一条消息
CLUSTERING 集群模式
同组内一个实例消费
一条消息
BROADCASTING 广播模式
实例 A 消费
实例 B 消费
实例 C 消费

两种消费模式适用于不同场景:

text 复制代码
普通业务处理:通常使用 CLUSTERING
每个实例都必须执行:使用 BROADCASTING

延迟消息

延迟消息不是立即投递,而是等待一段时间后再让消费者处理。

RocketMQ 4.x 常见延迟级别:

延迟级别 延迟时间
1 1 秒
2 5 秒
3 10 秒
4 30 秒
5 1 分钟

发送一条延迟 1 秒的消息:

java 复制代码
Message<String> message = MessageBuilder
        .withPayload(String.valueOf(contentId))
        .build();

rocketMQTemplate.asyncSend(
        "RedisDelayDeleteTopic",
        message,
        new SendCallback() {
            @Override
            public void onSuccess(SendResult result) {
                System.out.println("延迟消息发送成功");
            }

            @Override
            public void onException(Throwable ex) {
                System.out.println("延迟消息发送失败");
            }
        },
        3000,
        1 // 延迟级别 1,表示 1 秒
);

最后一个参数 1 表示延迟级别 1,不是延迟 1 毫秒。

RocketMQ 5.x 支持更灵活的定时消息,具体 API 取决于客户端和服务端版本。


Redis 延迟双删

先看一个问题。

查询请求和更新请求并发执行:
Redis 更新请求 数据库 查询请求 Redis 更新请求 数据库 查询请求 #mermaid-svg-qidphW3m0KvHgAPt{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qidphW3m0KvHgAPt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qidphW3m0KvHgAPt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qidphW3m0KvHgAPt .error-icon{fill:#552222;}#mermaid-svg-qidphW3m0KvHgAPt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qidphW3m0KvHgAPt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qidphW3m0KvHgAPt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qidphW3m0KvHgAPt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qidphW3m0KvHgAPt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qidphW3m0KvHgAPt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qidphW3m0KvHgAPt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qidphW3m0KvHgAPt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qidphW3m0KvHgAPt .marker.cross{stroke:#333333;}#mermaid-svg-qidphW3m0KvHgAPt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qidphW3m0KvHgAPt p{margin:0;}#mermaid-svg-qidphW3m0KvHgAPt .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-qidphW3m0KvHgAPt text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-qidphW3m0KvHgAPt .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-qidphW3m0KvHgAPt .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-qidphW3m0KvHgAPt .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-qidphW3m0KvHgAPt .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-qidphW3m0KvHgAPt #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-qidphW3m0KvHgAPt .sequenceNumber{fill:white;}#mermaid-svg-qidphW3m0KvHgAPt #sequencenumber{fill:#333;}#mermaid-svg-qidphW3m0KvHgAPt #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-qidphW3m0KvHgAPt .messageText{fill:#333;stroke:none;}#mermaid-svg-qidphW3m0KvHgAPt .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-qidphW3m0KvHgAPt .labelText,#mermaid-svg-qidphW3m0KvHgAPt .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-qidphW3m0KvHgAPt .loopText,#mermaid-svg-qidphW3m0KvHgAPt .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-qidphW3m0KvHgAPt .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-qidphW3m0KvHgAPt .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-qidphW3m0KvHgAPt .noteText,#mermaid-svg-qidphW3m0KvHgAPt .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-qidphW3m0KvHgAPt .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-qidphW3m0KvHgAPt .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-qidphW3m0KvHgAPt .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-qidphW3m0KvHgAPt .actorPopupMenu{position:absolute;}#mermaid-svg-qidphW3m0KvHgAPt .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-qidphW3m0KvHgAPt .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-qidphW3m0KvHgAPt .actor-man circle,#mermaid-svg-qidphW3m0KvHgAPt line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-qidphW3m0KvHgAPt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Redis 又出现旧数据 查询数据返回旧数据更新数据库删除 Redis 缓存把旧数据写回 Redis

虽然更新请求删除了 Redis,但查询请求随后又把旧数据写了回来。

解决思路是再延迟删除一次:

text 复制代码
更新数据库
-> 立即删除 Redis
-> 发送延迟消息
-> 1 秒后再次删除 Redis

更新时发送延迟消息

延迟 1s 发送只是说,一般情况下旧数据写回 Redis,1s 就够用了,基本上能保证这个延迟消息是在旧数据写回 Redis 之后执行的

java 复制代码
public void updateContent(Long contentId) {
    // 1. 更新数据库
    updateDatabase(contentId);

    String redisKey = "content:detail:" + contentId;

    // 2. 第一次删除 Redis
    redisTemplate.delete(redisKey);

    // 3. 发送延迟消息
    Message<String> message = MessageBuilder
            .withPayload(String.valueOf(contentId))
            .build();

    SendCallback sendCallback = new SendCallback() {
        @Override
        public void onSuccess(SendResult result) {
            log.info("延迟删除消息发送成功,contentId={}", contentId);
        }

        @Override
        public void onException(Throwable ex) {
            log.error("延迟删除消息发送失败,contentId={}",
                    contentId, ex);
        }
    };

    rocketMQTemplate.asyncSend(
            "RedisDelayDeleteTopic",
            message,
            sendCallback,
            3000,
            1
    );
}

消费延迟消息

java 复制代码
@Component
@RocketMQMessageListener(
        consumerGroup = "redis-delay-delete-group",
        topic = "RedisDelayDeleteTopic"
)
public class RedisDelayDeleteConsumer
        implements RocketMQListener<String> {

    @Override
    public void onMessage(String contentId) {
        String redisKey = "content:detail:" + contentId;

        // 第二次删除 Redis
        redisTemplate.delete(redisKey);
    }
}

第二次删除用于清理并发查询重新写入的旧缓存。

延迟双删不能保证严格强一致,只是缩短旧缓存存在的时间。延迟消息发送失败时,仍然要记录日志、告警或补偿任务。


消息消费失败怎么办

消费者发生异常时,不要只打印日志然后结束:

java 复制代码
@Override
public void onMessage(LikeEvent event) {
    try {
        saveLike(event);
    } catch (Exception ex) {
        log.error("消费失败", ex);
        throw ex;
    }
}

抛出异常后,RocketMQ 才知道本次消费失败,并按照配置重新投递。

消息可能被重复消费,所以消费者必须幂等。例如点赞表可以使用:

sql 复制代码
CREATE UNIQUE INDEX uk_user_content
ON content_like(user_id, content_id);

同一个用户对同一个内容重复点赞时,数据库唯一索引可以防止产生两条记录。


常用配置

除 NameServer 和 Producer Group 外,发送超时、重试次数、消息大小和拉取数量也是常用配置:

yaml 复制代码
rocketmq:
  name-server: 127.0.0.1:9876

  producer:
    group: demo-producer-group
    send-message-timeout: 3000
    retry-times-when-send-failed: 3
    retry-times-when-send-async-failed: 3
    max-message-size: 4194304

  consumer:
    pull-batch-size: 5
配置 作用
send-message-timeout 发送消息最多等待多久
retry-times-when-send-failed 同步发送失败重试次数
retry-times-when-send-async-failed 异步发送失败重试次数
max-message-size 最大消息体大小
pull-batch-size 每次拉取的最大消息数

消息不要放图片、视频等大文件,通常只发送业务 ID 和必要字段。


总结

Spring Boot 使用 RocketMQ 的核心链路是:生产者通过 RocketMQTemplate 将消息发送到 Topic,消费者组订阅 Topic,并在 onMessage 中完成业务处理。

不同业务需求对应不同消息模型:

text 复制代码
普通异步处理:asyncSend
同一业务主体按序处理:asyncSendOrderly + ORDERLY
所有实例都要执行:BROADCASTING
延后执行任务:延迟消息
相关推荐
罗超驿2 小时前
2.算法效率的核心密码:时间复杂度和空间复杂度详解
java·数据结构·算法
栈溢出了3 小时前
Redis 分片集群与哈希槽笔记
java·redis·笔记·spring
这里是杨杨吖3 小时前
SpringBoot+Vue心理健康咨询系统 附带详细运行指导视频
spring boot·vue·心理健康
遨游DATA3 小时前
Spring Boot 启动失败排查:如何区分多 Bean 冲突与清理阶段异常
java·spring boot·后端
雪碧聊技术3 小时前
Spring Boot项目如何彻底禁用Nacos(服务发现+配置中心),本地启动不再依赖Nacos
spring boot·nacos·服务发现
wabs6664 小时前
关于单调栈【力扣739.每日温度的思考】
java·开发语言
雪碧聊技术4 小时前
为什么要学函数式编程?
java·函数式编程
aaPIXa6224 小时前
C++ 用 13 条规则让模型写出安全现代 C++
java·c++·安全