請先閱讀之前的內容:
- Spring Cloud 學習筆記 - No.1 服務注冊發現
- Spring Cloud 學習筆記 - No.2 服務消費 Ribbon & Feign
- Spring Cloud 學習筆記 - No.3 分布式配置 Config
- Spring Cloud 學習筆記 - No.4 斷路器 Hystrix
- Spring Cloud 學習筆記 - No.5 服務網關 Zuul
- Spring Cloud 學習筆記 - No.6 通過 Swagger2 構建 API 文檔
Spring Cloud Stream
Spring Cloud Stream 是一個用來為微服務應用構建消息驅動能力的框架,為一些供應商的消息中間件產品提供了個性化的自動化配置實現,并且引入了發布-訂閱、消費組以及消息分區這三個核心概念。
簡單的說,Spring Cloud Stream 本質上就是整合了 Spring Boot 和 Spring Integration,實現了一套輕量級的消息驅動的微服務框架。
通過使用 Spring Cloud Stream,可以有效地簡化開發人員對消息中間件的使用復雜度,讓系統開發人員可以有更多的精力關注于核心業務邏輯的處理。目前為止 Spring Cloud Stream 只支持下面兩個消息中間件的自動化配置:
構建一個 Spring Cloud Stream 消費者
我們利用之前創建的 eureka-consumer
項目。
首先在 pom.xml
中添加如下的依賴:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
其中 spring-cloud-starter-stream-rabbit
是 Spring Cloud Stream 對 RabbitMQ 支持的封裝,其中包含了對 RabbitMQ 的自動化配置等內容。
隨后創建用于接收來自 RabbitMQ 消息的消費者 SinkReceiver
:
@EnableBinding(Sink.class)
public class SinkReceiver {
private static Logger logger = LoggerFactory.getLogger(SinkReceiver.class);
@StreamListener(Sink.INPUT)
public void receive(Object payload) {
logger.info("Received: " + payload);
}
}
-
@EnableBinding
注解用來指定一個或多個定義了@Input
或@Output
注解的接口,以此實現對消息通道(Channel)的綁定。- 綁定了
Sink
接口,該接口是 Spring Cloud Stream 中默認實現的對輸入消息通道綁定的定義 - Spring Cloud Stream 還默認實現了綁定輸出消息通道的
Source
接口 - 還有結合了
Sink
和Source
的Processor
接口
- 綁定了
-
@StreamListener
注解用來將被修飾的方法注冊為消息中間件上數據流的事件監聽器,注解中的屬性值對應了監聽的消息通道名。
重啟項目,從日志中可以看到聲明了一個名為 input.anonymous.cWlqMyH9Tm--INXERE6nhQ
的隊列,并通過 RabbitMessageChannelBinder
將自己綁定為它的消費者。
c.s.b.r.p.RabbitExchangeQueueProvisioner : declaring queue for inbound: input.anonymous.cWlqMyH9Tm--INXERE6nhQ, bound to: input
這些信息我們也能在 RabbitMQ 的控制臺中發現它們:
點擊進去,通過 Publish Message 功能來發送一條消息到該隊列中:
從下面的日志可以看出 SinkReceiver
讀取了消息隊列中的內容,由于我們沒有對消息進行序列化,所以輸出的只是該對象的引用:
[m--INXERE6nhQ-1] com.example.SinkReceiver : Received: [B@beb7ce8
在上面的操作中,我們并沒有手動去配置 RabbitMQ 的信息,比如 IP,端口等等,這是基于 Spring Boot 的設計理念,提供了對 RabbitMQ 默認的自動化配置。當然,我們可以手動在 application.properties
文件中去配置,例如:
spring.cloud.stream.bindings.input.destination=my_destination
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=root
spring.rabbitmq.password=root
編寫消費消息的單元測試用例
@RunWith(SpringRunner.class)
@EnableBinding(value = {SinkReceiverTests.SinkSender.class})
public class SinkReceiverTests {
@Autowired
private SinkSender sinkSender;
@Test
public void sinkSenderTester() {
sinkSender.output().send(MessageBuilder.withPayload("Testing Message").build());
}
public interface SinkSender {
String OUTPUT = "input";
@Output(SinkSender.OUTPUT)
MessageChannel output();
}
}
在上面的單元測試中,我們通過 @Output(SinkSender.OUTPUT)
定義了一個輸出通過,而該輸出通道的名稱為 input
,與前文中的 Sink
中定義的消費通道同名,所以這里的單元測試與前文的消費者程序組成了一對生產者與消費者。
運行該單元測試,日志可以看出 SinkReceiver
讀取了消息隊列中的內容:
[m--INXERE6nhQ-1] com.example.SinkReceiver : Received: [B@89040a9
Spring Cloud Stream 應用模型
圖片引自:https://docs.spring.io/spring-cloud-stream/docs/Fishtown.BUILD-SNAPSHOT/reference/htmlsingle/
綁定器
Spring Cloud Stream 構建的應用程序與消息中間件之間是通過綁定器: Binder 相關聯的,綁定器對于應用程序而言起到了隔離作用,它使得不同消息中間件的實現細節對應用程序來說是透明的。
當我們需要升級消息中間件,或是更換其他消息中間件產品時,我們要做的就是更換它們對應的 Binder 綁定器而不需要修改任何Spring Boot的應用邏輯。
所以對于每一個 Spring Cloud Stream 的應用程序來說,它不需要知曉消息中間件的通信細節,它只需要知道 Binder 對應用程序提供的概念去實現即可,而這個概念就是消息通道:Channel。
發布-訂閱模式
消息會通過共享的 Topic 主題進行廣播,消息消費者在訂閱的主題中收到它并觸發自身的業務邏輯處理。
這里所提到的 Topic 主題是 Spring Cloud Stream 中的一個抽象概念,用來代表發布共享消息給消費者的地方。
在不同的消息中間件中,Topic 可能對應著不同的概念,比如:在 RabbitMQ 中的它對應了 Exchange、而在 Kakfa 中則對應了 Kafka 中的 Topic。
在上面的例子中,應用啟動的時候,在 RabbitMQ 的 Exchange 中也創建了一個名為 input
的 Exchange交換器。例如我們分別以 3001 和 3002 兩個端口啟動 eureka-consumer
項目。
可以看到 Queues 中有兩個 Queue:
可以看到 Channels 中有兩個 Channel:
可以看出 Exchanges 中只有一個名稱為 input
的 Exchange,即 Topic 主題。但是點進去,可以看出這個名稱為 input
的 Exchange 有綁定了兩個消息隊列:
如果我們通過 Exchange 頁面的 Publish Message 來發布消息,可以發現兩個啟動的應用程序都輸出了消息內容。
圖片引自 http://blog.didispace.com/spring-cloud-starter-dalston-7-2/
相對于點對點隊列實現的消息通信來說,Spring Cloud Stream 采用的發布-訂閱模式可以有效的降低消息生產者與消費者之間的耦合,當我們需要對同一類消息增加一種處理方式時,只需要增加一個應用程序并將輸入通道綁定到既有的 Topic 中就可以實現功能的擴展,而不需要改變原來已經實現的任何內容。
消費組
很多情況下,消息生產者發送消息給某個具體微服務時,只希望被消費一次,但是上面我們啟動兩個應用(3001 和 3002 兩個端口),這個消息出現了被重復消費兩次的情況。
為了解決這個問題,在 Spring Cloud Stream 中提供了消費組的概念。
如果在同一個主題上的應用需要啟動多個實例的時候,我們可以通過spring.cloud.stream.bindings.<channelName>.group
屬性為應用指定一個組名,這樣這個應用的多個實例在接收到消息的時候,只會有一個成員真正的收到消息并進行處理。
例如,我們在 eureka-consumer
項目的配置中增加:
spring.cloud.stream.bindings.input.group=eureka-consumer-input-group
重啟兩個端口的實例,隨后通過 Exchange 頁面的 Publish Message 來發布消息,可以發現只有一個啟動的應用程序都輸出了消息內容。并且有時候是 3001 端口的實例處理,有時候是 3002 端口的實例處理。
也就是說,對于同一條消息,它多次到達之后可能是由不同的實例進行消費的。
消息分區
在上面的實驗中可以看到,消費組并無法控制消息具體被哪個實例消費。但是對于一些業務場景,就需要對于一些具有相同特征的消息每次都可以被同一個消費實例處理。比如:一些用于監控服務,為了統計某段時間內消息生產者發送的報告內容,監控服務需要在自身內容聚合這些數據,那么消息生產者可以為消息增加一個固有的特征 ID 來進行分區,使得擁有這些 ID 的消息每次都能被發送到一個特定的實例上實現累計統計的效果,否則這些數據就會分散到各個不同的節點導致監控結果不一致的情況。
而消息分區概念的引入就是為了解決這樣的問題:當生產者將消息數據發送給多個消費者實例時,保證擁有共同特征的消息數據始終是由同一個消費者實例接收和處理。
例如,我們在 eureka-consumer
項目的配置中增加:
spring.cloud.stream.bindings.input.consumer.partitioned=true
spring.cloud.stream.instanceCount=2
spring.cloud.stream.instanceIndex=0
-
spring.cloud.stream.bindings.input.consumer.partitioned
:通過該參數開啟消費者分區功能; -
spring.cloud.stream.instanceCount
:該參數指定了當前消費者的總實例數量; -
spring.cloud.stream.instanceIndex
:該參數設置當前實例的索引號,從0開始,最大值為spring.cloud.stream.instanceCount
- 1。
Spring Cloud Stream VS Spring Cloud Bus
我們在 Spring Cloud 學習筆記 - No.3 分布式配置 Config 中使用了 Spring Cloud Bus(結合了 RabbitMQ),那么 Stream 和 Bus 的區別是什么?
-
Spring Cloud Stream 構建消息驅動微服務
- building highly scalable event-driven microservices connected with shared messaging systems.
-
Spring Cloud Bus 廣播(例如配置統一管理)和監控
- Spring Cloud Bus links nodes of a distributed system with a lightweight message broker. This can then be used to broadcast state changes (e.g. configuration changes) or other management instructions.
RabbitMQ 負載均衡
在上面的例子中,我們始終只有一個 RabbitMQ 實例。在生產環境中,我們可能需要多個 RabbitMQ 實例來實現高并發和高可用。
參見:
引用:
程序猿DD Spring Cloud基礎教程
Spring Cloud構建微服務架構:消息驅動的微服務(入門)【Dalston版】
Spring Cloud構建微服務架構:消息驅動的微服務(核心概念)【Dalston版】
Spring Cloud Dalston中文文檔