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
延后执行任务:延迟消息