消息驅(qū)動的微服務: Spring Cloud Stream
1.官方定義:
Spring Cloud Stream 是一個構(gòu)建消息驅(qū)動微服務的框架。
應用程序通過 inputs
或者 outputs
來與 Spring Cloud Stream 中binder
交互,通過我們配置來 binding ,而 Spring Cloud Stream 的 binder 負責與消息中間件交互。所以,我們只需要搞清楚如何與 Spring Cloud Stream 交互就可以方便使用消息驅(qū)動的方式。
通過使用Spring Integration來連接消息代理中間件以實現(xiàn)消息事件驅(qū)動。Spring Cloud Stream 為一些供應商的消息中間件產(chǎn)品提供了個性化的自動化配置實現(xiàn),引用了發(fā)布-訂閱、消費組、分區(qū)的三個核心概念。目前僅支持RabbitMQ、Kafka。
什么是Spring Integration
? Integration 集成
企業(yè)應用集成(EAI)是集成應用之間數(shù)據(jù)和服務的一種應用技術(shù)。四種集成風格:
1.文件傳輸:兩個系統(tǒng)生成文件,文件的有效負載就是由另一個系統(tǒng)處理的消息。該類風格的例子之一是針對文件輪詢目錄或FTP目錄,并處理該文件。
2.共享數(shù)據(jù)庫:兩個系統(tǒng)查詢同一個數(shù)據(jù)庫以獲取要傳遞的數(shù)據(jù)。一個例子是你部署了兩個EAR應用,它們的實體類(JPA、Hibernate等)共用同一個表。
3.遠程過程調(diào)用:兩個系統(tǒng)都暴露另一個能調(diào)用的服務。該類例子有EJB服務,或SOAP和REST服務。
4.消息:兩個系統(tǒng)連接到一個公用的消息系統(tǒng),互相交換數(shù)據(jù),并利用消息調(diào)用行為。該風格的例子就是眾所周知的中心輻射式的(hub-and-spoke)JMS架構(gòu)。
2. 為什么需要SpringCloud Stream消息驅(qū)動呢?
比方說我們用到了RabbitMQ和Kafka,由于這兩個消息中間件的架構(gòu)上的不同,像RabbitMQ有exchange,kafka有Topic,partitions分區(qū),這些中間件的差異性導致我們實際項目開發(fā)給我們造成了一定的困擾,我們?nèi)绻昧藘蓚€消息隊列的其中一種,如果后續(xù)業(yè)務需求,我想往另外一種消息隊列進行遷移,這時候無疑就是一個災難性的,一大堆東西都要重新推倒重新做,因為它跟我們的系統(tǒng)耦合了,這時候springcloud Stream給我們提供了一種解耦合的方式。
- 應用模型
應用程序通過 inputs 或者 outputs 來與 Spring Cloud Stream 中Binder 交互,通過我們配置來綁定,而 Spring Cloud Stream 的 Binder 負責與中間件交互。所以,我們只需要搞清楚如何與 Spring Cloud Stream 交互就可以方便使用消息驅(qū)動的方式。
-
抽象綁定器(The Binder Abstraction)
Spring Cloud Stream實現(xiàn)Kafkat和RabbitMQ的Binder實現(xiàn),也包括了一個TestSupportBinder,用于測試。你也可以寫根據(jù)API去寫自己的Binder。
Spring Cloud Stream 同樣使用了Spring boot的自動配置,并且抽象的Binder使Spring Cloud Stream的應用獲得更好的靈活性,比如:我們可以在
application.yml
或application.properties
中指定參數(shù)進行配置使用Kafka或者RabbitMQ,而無需修改我們的代碼。
通過 Binder ,可以方便地連接中間件,可以通過修改application.yml中的spring.cloud.stream.bindings.input.destination
來進行改變消息中間件(對應于Kafka的topic,RabbitMQ的exchanges)
? 在這兩者間的切換甚至不需要修改一行代碼。
-
發(fā)布-訂閱(Persistent Publish-Subscribe Support)
如下圖是經(jīng)典的Spring Cloud Stream的 發(fā)布-訂閱 模型,生產(chǎn)者 生產(chǎn)消息發(fā)布在shared topic(共享主題)上,然后 消費者 通過訂閱這個topic來獲取消息
?
其中topic對應于Spring Cloud Stream中的destinations(Kafka 的topic,RabbitMQ的 exchanges)
官方文檔這塊原理說的有點深奧,詳見官方文檔
-
消費組(Consumer Groups)
盡管發(fā)布-訂閱 模型通過共享的topic連接應用變得很容易,但是通過創(chuàng)建特定應用的多個實例的來擴展服務的能力同樣重要,但是如果這些實例都去消費這條數(shù)據(jù),那么很可能會出現(xiàn)重復消費的問題,我們只需要同一應用中只有一個實例消費該消息,這時我們可以通過消費組來解決這種應用場景, 當一個應用程序不同實例放置在一個具有競爭關(guān)系的消費組中,組里面的實例中只有一個能夠消費消息
設置消費組的配置為
spring.cloud.stream.bindings.<channelName>.group
,下面舉一個DD博客中的例子:
下圖中,通過網(wǎng)絡傳遞過來的消息通過主題,按照分組名進行傳遞到消費者組中
此時可以通過
spring.cloud.stream.bindings.input.group=Group-A
或spring.cloud.stream.bindings.input.group=Group-B
進行指定消費組
所有訂閱指定主題的組都會收到發(fā)布消息的一個備份,每個組中只有一個成員會收到該消息;如果沒有指定組,那么默認會為該應用分配一個匿名消費者組,與所有其它組處于 訂閱-發(fā)布 關(guān)系中。
ps:也就是說如果管道沒有指定消費組,那么這個匿名消費組會與其它組一起消費消息,出現(xiàn)了重復消費的問題。
-
消費者類型(Consumer Types)
1)支持有兩種消費者類型:
- Message-driven (消息驅(qū)動型,有時簡稱為
異步
) - Polled (輪詢型,有時簡稱為
同步
)
在Spring Cloud 2.0版本前只支持 Message-driven這種異步類型的消費者,消息一旦可用就會傳遞,并且有一個線程可以處理它;當你想控制消息的處理速度時,可能需要用到同步消費者類型。
2)持久化
- Message-driven (消息驅(qū)動型,有時簡稱為
一般來說所有擁有訂閱主題的消費組都是持久化的,除了匿名消費組。 Binder的實現(xiàn)確保了所有訂閱關(guān)系的消費訂閱是持久的,一個消費組中至少有一個訂閱了主題,那么被訂閱主題的消息就會進入這個組中,無論組內(nèi)是否停止。
注意: 匿名訂閱本身是非持久化的,但是有一些Binder的實現(xiàn)(比如RabbitMQ)則可以創(chuàng)建非持久化的組訂閱
通常情況下,當有一個應用綁定到目的地的時候,最好指定消費消費組。擴展Spring Cloud Stream應用程序時,必須為每個輸入綁定指定一個使用者組。這樣做可以防止應用程序的實例接收重復的消息(除非需要這種行為,這是不尋常的)。
-
分區(qū)支持(Partitioning Support)
在消費組中我們可以保證消息不會被重復消費,但是在同組下有多個實例的時候,我們無法確定每次處理消息的是不是被同一消費者消費,分區(qū)的作用就是為了確保具有共同特征標識的數(shù)據(jù)由同一個消費者實例進行處理,當然前邊的例子是狹義的,通信代理(broken topic)也可以被理解為進行了同樣的分區(qū)劃分。Spring Cloud Stream 的分區(qū)概念是抽象的,可以為不支持分區(qū)Binder實現(xiàn)(例如RabbitMQ)也可以使用分區(qū)。
注意:要使用分區(qū)處理,你必須同時對生產(chǎn)者和消費者進行配置。
3.編程模型(Programming Model)
為了理解編程模型,需要熟悉下列 核心概念:
Destination Binders(目的地綁定器): 負責與外部消息系統(tǒng)集成交互的組件
Destination Bindings(目的地綁定):在外部消息系統(tǒng)和應用的生產(chǎn)者和消費者之間的橋梁(由Destination Binders創(chuàng)建)
Message (消息): 用于生產(chǎn)者、消費者通過Destination Binders溝通的規(guī)范數(shù)據(jù)。
Destination Binders(目的地綁定器): Destination Binders是Spring Cloud Stream與外部消息中間件提供了必要的配置和實現(xiàn)促進集成的擴展組件。集成了生產(chǎn)者和消費者的消息的路由、連接和委托、數(shù)據(jù)類型轉(zhuǎn)換、用戶代碼調(diào)用等。
盡管Binders幫我們處理了許多事情,我們?nèi)孕枰獙λM行配置。之后會講
Destination Bindings (目的地綁定) : 如前所述,Destination Bindings 提供連接外部消息中間件和應用提供的生產(chǎn)者和消費者中間的橋梁。
使用 @EnableBinding
注解打在一個配置類上來定義一個Destination Binding,這個注解本身包含有@Configuration
,會觸發(fā)Spring Cloud Stream的基本配置。
接下來的例子展示完全配置且正常運行的Spring Cloud Stream應用,由INPUT接收消息轉(zhuǎn)換成String 類型并打印在控制臺上,然后轉(zhuǎn)換出一個大寫的信息返回到OUTPUT中。
@SpringBootApplication
@EnableBinding(Processor.class)
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
@StreamListener(Processor.INPUT)
@SendTo(Processor.OUTPUT)
public String handle(String value) {
System.out.println("Received: " + value);
return value.toUpperCase();
}
}
通過SendTo注解將方法內(nèi)返回值轉(zhuǎn)發(fā)到其他消息通道中,這里因為沒有定義接收通道,提示消息已丟失,解決方法是新建一個接口,如下:
public interface MyPipe{
//方法1
@Input(Processor.OUTPUT) //這里使用Processor.OUTPUT是因為要同一個管道,或者名稱相同
SubscribableChannel input();
//還可以如下這樣=====二選一即可==========
//方法2
String INPUT = "output";
@Input(MyPipe.INPUT)
SubscribableChannel input();
}
然后在在上邊的方法下邊加一個方法,并在@EnableBinding
注解中改成
@EnableBinding({Processor.class, MyPipe.class})
@StreamListener(MyPipe.INPUT)
public void handleMyPipe(String value) {
System.out.println("Received: " + value);
}
Spring Cloud Stream已經(jīng)為我們提供了三個綁定消息通道的默認實現(xiàn)
Sink:通過指定消費消息的目標來標識消息使用者的約定。
Source:與Sink相反,用于標識消息生產(chǎn)者的約定。
Processor:集成了Sink和Source的作用,標識消息生產(chǎn)者和使用者
他們的源碼分別為:
public interface Sink {
String INPUT = "input";
@Input("input")
SubscribableChannel input();
}
public interface Source {
String OUTPUT = "output";
@Output("output")
MessageChannel output();
}
public interface Processor extends Source, Sink {
}
Sink和Source中分別通過@Input和@Output注解定義了輸入通道和輸出通道,通過使用這兩個接口中的成員變量來定義輸入和輸出通道的名稱,Processor由于繼承自這兩個接口,所以同時擁有這兩個通道。
注意點:擁有多條管道的時候不能有輸入輸出管道名相同的,否則會出現(xiàn)發(fā)送消息被自己接收或報錯的情況
我們可以根據(jù)上述源碼的方式來定義我們自己的輸入輸出通道,定義輸入通道需要返回SubscribaleChannel
接口對象,這個接口繼承自 MessageChannel
接口,它定義了維護消息通道訂閱者的方法;定義輸出通道則需要返回MessageChannel接口對象,它定義了向消息通道發(fā)送消息的方法。
自定義消息通道 發(fā)送與接收
依照上面的內(nèi)容,我們也可以創(chuàng)建自己的綁定通道 如果你實現(xiàn)了上邊的MyPipe接口,那么直接使用這個接口就好, 和主類同包下建一個MyPipe接口,實現(xiàn)如下:
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.messaging.SubscribableChannel;
public interface MyPipe {
//方法1
// @Input(Source.OUTPUT) //Source.OUTPUT的值是output,我們自定義也是一樣的
// SubscribableChannel input(); //使用@Input注解標注的輸入管道需要使用SubscribableChannel來訂閱通道
//========二選一使用===========
//方法2
String INPUT = "output";
@Input(MyPipe.INPUT)
SubscribableChannel input();
}
這里用Source.OUTPUT和第二種方法 是一樣的,我們只要將消息發(fā)送到名為output的管道中,那么監(jiān)聽output管道的輸入流一端就能獲得數(shù)據(jù)
擴展主類,添加監(jiān)聽output管道方法
@StreamListener(MyPipe.INPUT)
public void receiveFromMyPipe(Object payload){
logger.info("Received: "+payload);
}
在主類的頭上的@EnableBinding改為@EnableBinding({Sink.class, MyPipe.class}),加入了Mypipe接口的綁定
在test/java下創(chuàng)建com.cnblogs.hellxz,并在包下新建一個測試類,如下:
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@EnableBinding(value = {Source.class})
@SpringBootTest
public class TestSendMessage {
@Autowired
private Source source; //注入接口和注入MessageChannel的區(qū)別在于發(fā)送時需不需要調(diào)用接口內(nèi)的方法
@Test
public void testSender() {
source.output().send(MessageBuilder.withPayload("Message from MyPipe").build());
//假設注入了MessageChannel messageChannel; 因為綁定的是Source這個接口,
//所以會使用其中的唯一產(chǎn)生MessageChannel的方法,那么下邊的代碼會是
//messageChannel.send(MessageBuilder.withPayload("Message from MyPipe").build());
}
}
啟動主類,清空輸出,運行測試類,然后你就會得到在主類的控制臺的消息以log形式輸出Message from MyPipe
通過注入消息通道,并調(diào)用他的output方法聲明的管道獲得的MessageChannel
實例,發(fā)送的消息,管道注入過程中可能會出現(xiàn)的問題,通過注入消息通道的方式雖然很直接,但是也容易犯錯,當一個接口中有多個通道的時候,他們返回的實例都是MessageChannel,這樣通過@Autowired
注入的時候往往會出現(xiàn)有多個實例找到無法確定需要注入實例的錯誤,我們可以通過@Qualifier指定消息通道的名稱,下面舉例:
在主類包內(nèi)創(chuàng)建一個擁有多個輸出流的管道
/**
* 多個輸出管道
*/
public interface MutiplePipe {
@Output("output1")
MessageChannel output1();
@Output("output2")
MessageChannel output2();
}
創(chuàng)建一個測試類
@RunWith(SpringRunner.class)
@EnableBinding(value = {MutiplePipe.class}) //開啟綁定功能
@SpringBootTest //測試
public class TestMultipleOutput {
@Autowired
private MessageChannel messageChannel;
@Test
public void testSender() {
//向管道發(fā)送消息
messageChannel.send(MessageBuilder.withPayload("produce by multiple pipe").build());
}
}
啟動測試類,會出現(xiàn)剛才說的不唯一的bean,無法注入
Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'org.springframework.messaging.MessageChannel' available: expected single matching bean but found 6: output1,output2,input,output,nullChannel,errorChannel
我們在@Autowired旁邊加上@Qualifier("output1"),然后測試就可以正常啟動了
通過上邊的錯誤,我們可以清楚的看到,每個MessageChannel都是使用消息通道的名字做為bean的名稱。
這里我們沒有使用監(jiān)聽這個管道,僅為了測試并發(fā)現(xiàn)問題
常用配置
消費組和分區(qū)的設置
給消費者設置消費組和主題
1.設置消費組: spring.cloud.stream.bindings.<通道名>.group=<消費組名>
2.設置主題: spring.cloud.stream.bindings.<通道名>.destination=<主題名>
給生產(chǎn)者指定通道的主題:spring.cloud.stream.bindings.<通道名>.destination=<主題名>
消費者開啟分區(qū),指定實例數(shù)量與實例索引
1.開啟消費分區(qū): spring.cloud.stream.bindings.<通道名>.consumer.partitioned=true
2.消費實例數(shù)量: spring.cloud.stream.instanceCount=1 (具體指定)
實例索引: spring.cloud.stream.instanceIndex=1
#設置當前實例的索引值
生產(chǎn)者指定分區(qū)鍵
1.分區(qū)鍵: spring.cloud.stream.bindings.<通道名>.producer.partitionKeyExpress=<分區(qū)鍵>
2.分區(qū)數(shù)量: spring.cloud.stream.bindings.<通道名>.producer.partitionCount=<分區(qū)數(shù)量>
Less is more.