SpringBoot實(shí)現(xiàn)發(fā)布/訂閱廣播消息的示例代碼
引言
在 RabbitMQ 的五大工作模式中,發(fā)布/訂閱(Publish/Subscribe)廣播模式是分布式系統(tǒng)中非常核心的通信方式。
我們?nèi)粘J褂玫钠胀c(diǎn)對點(diǎn)隊(duì)列,一條消息只會被一個消費(fèi)者消費(fèi)(競爭消費(fèi));而廣播模式可以實(shí)現(xiàn)一條消息、多服務(wù)、多消費(fèi)者同時接收,完美實(shí)現(xiàn)一對多通知。
像 緩存刷新、配置更新、全局通知、多節(jié)點(diǎn)日志同步、服務(wù)狀態(tài)廣播 等場景,全部依賴 Fanout 廣播模式。
一、什么是 MQ 廣播(發(fā)布/訂閱)模式?
1. 核心定義
廣播模式基于 FanoutExchange(扇形交換機(jī)) 實(shí)現(xiàn),核心邏輯:
生產(chǎn)者發(fā)送一條消息到 Fanout 交換機(jī),所有綁定該交換機(jī)的隊(duì)列,都會完整收到這條消息。
不管路由鍵是什么、不管隊(duì)列名稱,只要完成綁定,就會無條件廣播投遞。
2. 核心特性
- 無視routingKey,路由鍵傳空、傳任意值都不生效
- 純廣播、全量投遞、一對多分發(fā)
- 每條消息獨(dú)立進(jìn)入每一個綁定隊(duì)列
- 天然支持多服務(wù)、多節(jié)點(diǎn)同步通知
- 無匹配規(guī)則,綁定即接收
3. 適用業(yè)務(wù)場景
- 分布式緩存全局刷新(多節(jié)點(diǎn)統(tǒng)一清空緩存)
- 系統(tǒng)配置動態(tài)推送、熱更新
- 全站公告、全局消息推送
- 微服務(wù)多節(jié)點(diǎn)日志采集、鏈路追蹤
- 服務(wù)上下線、狀態(tài)同步廣播
- 多端消息同步(PC/APP/小程序)
二、四大交換機(jī)模式核心對比
| 交換機(jī)類型 | 匹配規(guī)則 | 消費(fèi)模式 | 核心場景 |
| Direct(直連) | 完全匹配 routingKey | 點(diǎn)對點(diǎn)競爭消費(fèi) | 訂單、支付、任務(wù)處理 |
| Topic(主題) | 通配符模糊匹配 | 選擇性多消費(fèi) | 日志分級、消息訂閱 |
| Fanout(廣播) | 無視路由鍵,全部投遞 | 全員訂閱消費(fèi) | 緩存刷新、全局通知 |
| Headers | 匹配消息頭參數(shù) | 自定義匹配 | 極少使用 |
三、關(guān)鍵認(rèn)知誤區(qū)
1:同一個隊(duì)列多消費(fèi)者可以實(shí)現(xiàn)廣播
絕對錯誤!
同一個隊(duì)列下的多個消費(fèi)者,默認(rèn)是競爭消費(fèi),一條消息只會被一個消費(fèi)者消費(fèi)。
廣播必備條件:每個消費(fèi)者對應(yīng)一個獨(dú)立隊(duì)列,全部綁定同一個 Fanout 交換機(jī)。
2:Fanout 交換機(jī)需要配置路由鍵
Fanout 交換機(jī)底層邏輯直接忽略 routingKey,無論發(fā)送時傳什么值,都不會影響廣播效果。
3:廣播消息天然可靠、不會丟失
默認(rèn)非持久化、自動ACK 場景下,廣播消息極易丟失,生產(chǎn)必須做持久化+手動ACK。
四、SpringBoot 完整實(shí)現(xiàn)
1. 基礎(chǔ)依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>2. 生產(chǎn)級配置文件
spring:
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
virtual-host: /
listener:
simple:
# 手動ACK 保證廣播消息不丟
acknowledge-mode: manual
# 限制預(yù)取數(shù),防止單節(jié)點(diǎn)消息堆積
prefetch: 5
# 開啟消費(fèi)重試
retry:
enabled: true
max-attempts: 3
initial-interval: 10003. 廣播交換機(jī)、隊(duì)列、綁定配置類
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FanoutBroadcastConfig {
// 廣播交換機(jī)名稱
public static final String FANOUT_EXCHANGE = "system.fanout.broadcast.exchange";
// 三個獨(dú)立消費(fèi)者隊(duì)列
public static final String QUEUE_CACHE_REFRESH = "queue.cache.refresh";
public static final String QUEUE_NOTICE = "queue.system.notice";
public static final String QUEUE_LOG = "queue.log.collect";
// 聲明 Fanout 廣播交換機(jī):持久化、不自動刪除
@Bean
public FanoutExchange fanoutExchange() {
return new FanoutExchange(FANOUT_EXCHANGE, true, false);
}
// 隊(duì)列1:緩存刷新隊(duì)列
@Bean
public Queue cacheRefreshQueue() {
return new Queue(QUEUE_CACHE_REFRESH, true);
}
// 隊(duì)列2:系統(tǒng)通知隊(duì)列
@Bean
public Queue noticeQueue() {
return new Queue(QUEUE_NOTICE, true);
}
// 隊(duì)列3:日志采集隊(duì)列
@Bean
public Queue logQueue() {
return new Queue(QUEUE_LOG, true);
}
// 全部綁定到廣播交換機(jī)
@Bean
public Binding bindingCacheRefresh(Queue cacheRefreshQueue, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(cacheRefreshQueue).to(fanoutExchange);
}
@Bean
public Binding bindingNotice(Queue noticeQueue, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(noticeQueue).to(fanoutExchange);
}
@Bean
public Binding bindingLog(Queue logQueue, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(logQueue).to(fanoutExchange);
}
}4. 廣播消息生產(chǎn)者
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class BroadcastProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/send/broadcast")
public String sendBroadcastMsg(@RequestParam String content) {
// Fanout廣播:路由鍵傳空字符串
rabbitTemplate.convertAndSend(
FanoutBroadcastConfig.FANOUT_EXCHANGE,
"",
content
);
return "? 廣播消息發(fā)送成功:" + content;
}
}5. 多消費(fèi)者實(shí)現(xiàn)(全員接收)
消費(fèi)者1:緩存刷新消費(fèi)者
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class CacheRefreshConsumer {
@RabbitListener(queues = FanoutBroadcastConfig.QUEUE_CACHE_REFRESH)
public void consume(String msg, Message message, Channel channel) throws IOException {
try {
System.out.println("【緩存服務(wù)】接收廣播消息:" + msg);
// 執(zhí)行緩存刷新業(yè)務(wù)邏輯
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
// 消費(fèi)失敗,重回隊(duì)列重試
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
}
}
}消費(fèi)者2:系統(tǒng)通知消費(fèi)者
@Component
public class SystemNoticeConsumer {
@RabbitListener(queues = FanoutBroadcastConfig.QUEUE_NOTICE)
public void consume(String msg, Message message, Channel channel) throws IOException {
try {
System.out.println("【通知服務(wù)】接收廣播消息:" + msg);
// 執(zhí)行消息推送業(yè)務(wù)
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
}
}
}消費(fèi)者3:日志采集消費(fèi)者
@Component
public class LogCollectConsumer {
@RabbitListener(queues = FanoutBroadcastConfig.QUEUE_LOG)
public void consume(String msg, Message message, Channel channel) throws IOException {
try {
System.out.println("【日志服務(wù)】接收廣播消息:" + msg);
// 執(zhí)行日志采集業(yè)務(wù)
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
}
}
}五、測試效果
訪問接口:
http://localhost:8080/send/broadcast?content=全局緩存刷新通知
控制臺輸出:
【緩存服務(wù)】接收廣播消息:全局緩存刷新通知 【通知服務(wù)】接收廣播消息:全局緩存刷新通知 【日志服務(wù)】接收廣播消息:全局緩存刷新通知
一條消息,多服務(wù)同時消費(fèi),廣播生效!
六、總結(jié)
1. 必須開啟持久化
交換機(jī)、隊(duì)列全部設(shè)置持久化,防止重啟丟失廣播配置。
2. 強(qiáng)制手動ACK
廣播場景多為重要通知、緩存同步,自動ACK會導(dǎo)致業(yè)務(wù)未執(zhí)行完成消息丟失。
3. 每個服務(wù)獨(dú)立隊(duì)列
不同微服務(wù)必須使用獨(dú)立隊(duì)列,避免競爭消費(fèi),保證廣播全覆蓋。
4. 廣播消息建議做冪等
MQ 重試、網(wǎng)絡(luò)抖動會導(dǎo)致廣播消息重復(fù)推送,核心業(yè)務(wù)必須基于消息ID做冪等防重。
5. 禁止設(shè)置復(fù)雜路由鍵
Fanout 無視路由鍵,統(tǒng)一傳空字符串,保持代碼規(guī)范。
寫在最后
廣播發(fā)布訂閱模式是微服務(wù)分布式通信的重要基石,區(qū)別于傳統(tǒng)的點(diǎn)對點(diǎn)任務(wù)消費(fèi),它主打全局通知、多節(jié)點(diǎn)同步、狀態(tài)廣播,是緩存刷新、配置熱更新、系統(tǒng)公告等場景的最優(yōu)解。
很多開發(fā)者一直混淆“競爭消費(fèi)”和“廣播消費(fèi)”的本質(zhì),導(dǎo)致線上通知不全、同步失效等隱性問題。吃透 Fanout 交換機(jī)的底層原理與落地規(guī)范,能幫你徹底解決分布式多節(jié)點(diǎn)同步難題。
以上就是SpringBoot實(shí)現(xiàn)發(fā)布/訂閱廣播消息的示例代碼的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot發(fā)布/訂閱廣播消息的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Apache?Commons?Imaging處理圖像實(shí)例深究
這篇文章主要為大家介紹了Apache?Commons?Imaging處理圖像的實(shí)例探索深究,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12
Java中的Callable實(shí)現(xiàn)多線程詳解
這篇文章主要介紹了Java中的Callable實(shí)現(xiàn)多線程詳解,接口Callable中有一個call方法,其返回值類型為V,這是一個泛型,值得關(guān)注的是這個call方法有返回值,這意味著線程執(zhí)行完畢后可以將處理結(jié)果返回,需要的朋友可以參考下2023-08-08
java8 對象轉(zhuǎn)Map時重復(fù) key Duplicate key xxxx的解決
這篇文章主要介紹了java8 對象轉(zhuǎn)Map時重復(fù) key Duplicate key xxxx的解決方案,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-09-09
淺談java實(shí)現(xiàn)背包算法(0-1背包問題)
本篇文章主要介紹了淺談java實(shí)現(xiàn)背包算法(0-1背包問題) ,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-08-08
SpringBoot中的PropertySource原理詳解
這篇文章主要介紹了SpringBoot中的PropertySource原理詳解,PropertySource?是一個非常重要的概念,它允許您在應(yīng)用程序中定義屬性,并將這些屬性注入到?Spring?環(huán)境中,需要的朋友可以參考下2023-07-07

