官方定義 Spring Cloud Stream 是一個構建消息驅動微服務的框架。
應用程序通過 inputs 或者 outputs 來與 Spring Cloud Stream 中binder 交互,通過我們配置來 binding ,而 Spring Cloud Stream 的 binder 負責與消息中間件交互。所以,我們只需要搞清楚如何與 Spring Cloud Stream 交互就可以方便使用消息驅動的方式。
通過使用Spring Integration來連接消息代理中間件以實現消息事件驅動。Spring Cloud Stream 為一些供應商的消息中間件產品提供了個性化的自動化配置實現,引用了發布-訂閱、消費組、分區的三個核心概念。目前僅支持RabbitMQ、Kafka。
這里還要講解一下什么是Spring Integration ? Integration 集成
企業應用集成(EAI)是集成應用之間數據和服務的一種應用技術。四種集成風格:
1.文件傳輸:兩個系統生成文件,文件的有效負載就是由另一個系統處理的消息。該類風格的例子之一是針對文件輪詢目錄或FTP目錄,并處理該文件。
2.共享數據庫:兩個系統查詢同一個數據庫以獲取要傳遞的數據。一個例子是你部署了兩個EAR應用,它們的實體類(JPA、Hibernate等)共用同一個表。
3.遠程過程調用:兩個系統都暴露另一個能調用的服務。該類例子有EJB服務,或SOAP和REST服務。
4.消息:兩個系統連接到一個公用的消息系統,互相交換數據,并利用消息調用行為。該風格的例子就是眾所周知的中心輻射式的(hub-and-spoke)JMS架構。
為什么需要SpringCloud Stream消息驅動呢?
比方說我們用到了RabbitMQ和Kafka,由于這兩個消息中間件的架構上的不同,像RabbitMQ有exchange,kafka有Topic,partitions分區,這些中間件的差異性導致我們實際項目開發給我們造成了一定的困擾,我們如果用了兩個消息隊列的其中一種,后面的業務需求,我想往另外一種消息隊列進行遷移,這時候無疑就是一個災難性的,一大堆東西都要重新推倒重新做,因為它跟我們的系統耦合了,這時候springcloud Stream給我們提供了一種解耦合的方式。
Spring Cloud Stream 是一個構建消息驅動微服務的框架。應用程序通過 inputs 或者 outputs 來與 Spring Cloud Stream 中binder 交互,通過我們配置來 binding ,而 Spring Cloud Stream 的 binder 負責與中間件交互。所以,我們只需要搞清楚如何與 Spring Cloud Stream 交互就可以方便使用消息驅動的方式。
Spring Cloud Stream由一個中間件中立的核組成。應用通過Spring Cloud Stream插入的input(相當于消費者consumer,它是從隊列中接收消息的)和output(相當于生產者producer,它是從隊列中發送消息的。)通道與外界交流。
通道通過指定中間件的Binder實現與外部代理連接。業務開發者不再關注具體消息中間件,只需關注Binder對應用程序提供的抽象概念來使用消息中間件實現業務即可。
-
Binder
Binder 是 Spring Cloud Stream 的一個抽象概念,是應用與消息中間件之間的粘合劑。目前 Spring Cloud Stream 實現了 Kafka 和 Rabbit MQ 的binder。
通過 binder ,可以很方便的連接中間件,可以動態的改變消息的destinations(對應于 Kafka 的topic,Rabbit MQ 的 exchanges),這些都可以通過外部配置項來做到。甚至可以任意的改變中間件的類型而不需要修改一行代碼。
-
Publish-Subscribe
消息的發布(Publish)和訂閱(Subscribe)是事件驅動的經典模式。Spring Cloud Stream 的數據交互也是基于這個思想。生產者把消息通過某個 topic 廣播出去(Spring Cloud Stream 中的 destinations)。其他的微服務,通過訂閱特定 topic 來獲取廣播出來的消息來觸發業務的進行。
這種模式,極大的降低了生產者與消費者之間的耦合。即使有新的應用的引入,也不需要破壞當前系統的整體結構。
-
Consumer Groups
“Group”,如果使用過 Kafka 的童鞋并不會陌生。Spring Cloud Stream 的這個分組概念的意思基本和 Kafka 一致。
微服務中動態的縮放同一個應用的數量以此來達到更高的處理能力是非常必須的。對于這種情況,同一個事件防止被重復消費,只要把這些應用放置于同一個 “group” 中,就能夠保證消息只會被其中一個應用消費一次。
-
Consumer Groups
bindings 是我們通過配置把應用和spring cloud stream 的 binder 綁定在一起,之后我們只需要修改 binding 的配置來達到動態修改topic、exchange、type等一系列信息而不需要修改一行代碼。
Demo演練
這里新建3個模塊stream-Publish、stream-Subscribe1、stream-Subscribe2,其中stream-Publish作為消息發布模塊,stream-Subscribe1和stream-Subscribe2作為消息消費模塊(沒有kafka的,windows下載鏈接:http://kafka.apache.org/downloads內帶zookeeper)
springcloud版本:Hoxton.SR3
3個項目都需要以下依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-kafka</artifactId>
</dependency>
stream-Publish的yml配置
server:
port: 7888
spring:
application:
name: producer
cloud:
stream:
kafka:
binder:
brokers: localhost:9092 #Kafka的消息中間件服務器
zk-nodes: localhost:2181 #Zookeeper的節點,如果集群,后面加,號分隔
auto-create-topics: true #如果設置為false,就不會自動創建Topic 有可能你Topic還沒創建就直接調用了。
bindings:
output: #這里用stream給我們提供的默認output,后面會講到自定義output
destination: stream-demo #消息發往的目的地
content-type: text/plain #消息發送的格式,接收端不用指定格式,但是發送端要
stream-Publish的SendService
@EnableBinding(Source.class)
public class SendService {
@Autowired
private Source source;
public void sendMsg(String msg) { h
source.output().send(MessageBuilder.withPayload(msg).build());
}
}
stream-Publish的測試Controller
@RestController
@SpringBootApplication
public class DemoApplication {
@Autowired
private SendService sendService;
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@GetMapping("/send/{msg}")
public void send(@PathVariable("msg") String msg){
sendService.sendMsg(msg);
}
}
然后需要來寫消息消費者的代碼,stream-Subscribe1、stream-Subscribe2(同前一個,換個端口)
stream-Subscribe1的yml文件
server:
port: 7889
spring:
application:
name: consumer_1
cloud:
stream:
kafka:
binder:
brokers: localhost:9092
zk-nodes: localhost:2181
auto-create-topics: true
bindings:
#input是接收,注意這里不能再像前面一樣寫output了
input:
destination: stream-demo
stream-Subscribe1的接受消息的Service(啟動類默認就OK)
@EnableBinding(Sink.class)
public class RecieveService {
@StreamListener(Sink.INPUT)
public void recieve(Object payload){
System.out.println(payload);
}
}
然后我們啟動zookeeper,和Kafka,然后啟動這三個項目
訪問我們stream-Publish的測試接口,如下:
請求成功后我們可以看到消息消費者的控制臺打印出消息hello
自定義信道
好了到現在為止,我們進行了一個簡單的消息發送和接收,用的是Stream給我們提供的默認Source,Sink,接下來我們要自己進行自定義,這種方式在工作中還是用的比較多的,因為我們要往不同的消息通道發消息,必然不能全都叫input,output的,那樣的話就亂套了,因此首先自定義一個接口,如下:
Source(發射器): 一個接口類,內部定義了一個輸出管道,例如定義一個輸出管道 @output("XXOO")。說明這個發射器將會向這個管道發射數據。
Sink(接收器):一個接口類,內部定義了一個輸入管道,例如定義一個輸入管道 @input("XXOO")。說明這個接收器將會從這個管道接收數據。
Binder(綁定器):用于與管道進行綁定。Binder將于消息中間件進行關聯。@ EnableBinding (Source.class/Sink.class)。@EnableBinding()里面是可以定義多個發射器/接收器
自定義MySource:
在stream-Publish中創建自定義接口
public interface MySource {
@Output("myOutput")//管道名稱為"myOutput,對應在yml文件里
MessageChannel myOutput();
}
修改stream-Publish中的SendService文件
@EnableBinding(MySource.class) //使用我們自定義的Mysource
public class SendService {
@Autowired
private MySource mySource;
public void sendMsg(String msg) {
mySource.myOutput().send(MessageBuilder.withPayload(msg).build());
}
}
修改stream-Publish中yml文件
server:
port: 7888
spring:
application:
name: producer
cloud:
stream:
kafka:
binder:
brokers: localhost:9092 #Kafka的消息中間件服務器
zk-nodes: localhost:2181 #Zookeeper的節點,如果集群,后面加,號分隔
auto-create-topics: true #如果設置為false,就不會自動創建Topic 有可能你Topic還沒創建就直接調用了。
bindings:
myOutput: #自定義output
destination: stream-demo #消息發往的目的地
content-type: text/plain #消息發送的格式,接收端不用指定格式,但是發送端要 #這里用stream給我們提供的默認output,后面會講到自定義output
到這里,我們的消息發送服務已經修改完啦,接下來修改消息消費服務stream-Subscribe1,類似于上面,之類我直接貼代碼了:
public interface MySink {
@Input("myInput")
SubscribableChannel myInput();
}
@EnableBinding(MySink.class)
public class RecieveService {
@StreamListener("myInput")
public void recieve(Object payload){
System.out.println(payload);
}
}
server:
port: 7889
spring:
application:
name: consumer_1
cloud:
stream:
kafka:
binder:
brokers: localhost:9092
zk-nodes: localhost:2181
auto-create-topics: true
bindings:
#修改為我們自己的myInput
myInput:
destination: stream-demo
然后測試成功,和上面一樣就不放圖了
消息分組(Consumer Groups)
“Group”,如果使用過 Kafka 的讀者并不會陌生。Spring Cloud Stream 的這個分組概念的意思基本和 Kafka 一致。微服務中動態的縮放同一個應用的數量以此來達到更高的處理能力是非常必須的。對于這種情況,同一個事件防止被重復消費,
只要把這些應用放置于同一個 “group” 中,就能夠保證消息只會被其中一個應用消費一次。不同的組是可以消費的,同一個組內會發生競爭關系,只有其中一個可以消費。
我們只需要修改yml文件就可以啦,兩個消費者服務都配置相同的名稱的group
server:
port: 7889
spring:
application:
name: consumer_1
cloud:
stream:
kafka:
binder:
brokers: localhost:9092
zk-nodes: localhost:2181
auto-create-topics: true
bindings:
input:
destination: stream-demo
group: group #加上一條group就可以啦
可以看到stream-Subscribe1和stream-Subscribe2是屬于同一組的。springcloud-stream模塊的發的消息只能被stream-Subscribe1或stream-Subscribe2其中一個接收到,這樣避免了重復消費。
消息分區
Spring Cloud Stream對給定應用的多個實例之間分隔數據予以支持。在分隔方案中,物理交流媒介(如:代理主題)被視為分隔成了多個片(partitions)。一個或者多個生產者應用實例給多個消費者應用實例發送消息并確保相同特征的數據被同一消費者實例處理。
Spring Cloud Stream對分割的進程實例實現進行了抽象。使得Spring Cloud Stream 為不具備分區功能的消息中間件(RabbitMQ)也增加了分區功能擴展。
那么我們就要進行一些配置了,比如我只想要stream-Subscribe2模塊接收到消息,stream-Subscribe2配置如下:
server:
port: 7890
spring:
application:
name: consumer_2
cloud:
stream:
kafka:
binder:
brokers: localhost:9092
zk-nodes: localhost:2181
auto-create-topics: true
bindings:
input:
destination: stream-demo
group: group
consumer:
partitioned: true #開啟分區
instance-count: 2 #分區數量
stream-Publish模塊配置如下:
server:
port: 7888
spring:
application:
name: producer
cloud:
stream:
kafka:
binder:
brokers: localhost:9092
zk-nodes: localhost:2181
auto-create-topics: true
bindings:
myOutput:
destination: stream-demo
content-type: text/plain
producer:
partitionKeyExpression: payload.id(你自己的key) #分區的主鍵,根據什么來分區,下面的payload.id只是一個對象的id用于做為Key,用來說明的。希望不要誤解
partitionCount: 2 #Key和分區數量進行取模去分配消息,這里分區數量配置為2
其他的代碼基本不變,這里就不演示了。這里要給大家說明一下,比如分區的Key是一個對象的id,比如說id=1,每次發送消息的對象的id為相同值1,則消息只會被同一個消費者消費,比如說Key和分區數量取模計算的結果是分到stream2模塊中,那么下一次進行進行消息發送,
只要分組的key即id的值依然還是1的話,消息永遠只會分配到stream2模塊中。