【SpringCloud】SpringCloud Stream訊息驅動
SpringCloud Stream訊息驅動
訊息驅動概述
是什麼
什麼是SpringCloudStream
官方定義Spring Cloud Stream是一個構建訊息驅動微服務的框架。
應用程式通過inputs或者outputs與Spring Cloud Stream中binder物件互動。
通過我們配置來binding(繫結),而Spring Cloud Stream的binder物件負責與訊息中介軟體互動。
所以,我們只需要搞清楚如何與Spring Cloud Stream父互就可以方便使用訊息驅動的方式。
通過使用Spring Integration來連線訊息代理中介軟體以實現訊息事件驅動。
目前僅支援RabbitMQ、Kafka。
一句話
遮蔽底層訊息中介軟體的差異,降低切換成本,統一訊息的程式設計模型
官網
https://spring.io/projects/spring-cloud-stream
Spring Cloud Stream中文指導手冊
https://blog.csdn.net/qq_32734365/article/details/81413218#spring-cloud-stream%E4%B8%AD%E6%96%87%E6%8C%87%E5%AF%BC%E6%89%8B%E5%86%8C
設計思想
標準MQ
生產者/消費者之間靠訊息媒介傳遞資訊內容
Message
訊息必須走特定的通道
訊息通道MessageChannel
訊息通道里的訊息如何被消費呢,誰負責收發處理
訊息通道MessageChannel的子介面SubscribableChannel,由MessageHandler訊息處理器所訂閱
為什麼使用Cloud Stream
比方說我們用到了RabbitMQ和Kafka,於這兩個訊息中介軟體的架構上的不同,
像RabbitMQ有exchange,kafka有 Topic和Partitions分割槽,
這些中介軟體的差異性導致我們實際專案開發給我們造成了一定的困擾,我們如果用了兩個訊息佇列的其中一種, 後面的業務需求,我想往另外-種訊息佇列進行遷移,這時候無疑就是一個災難性的, -大堆東西都要重新推倒重新做,因為它跟我們的系統耦合了,這時候springcloud Stream給我們提供了一種解耦合的方式。
stream憑什麼可以統一底層差異
在沒有繫結器這個概念的情況下,我們的SpringBoot應要直接與訊息中介軟體進行資訊互動的時候,
由於各訊息中介軟體構建的初衷不同,它們的實現細節上會有較大的差異性
通過定義繫結器作為中間層,完美地實現了應用程式與訊息中介軟體細節之間的隔離。
通過嚮應用程式暴露統一的Channel通道, 使得應用程式不需要再考慮各種不同的訊息中介軟體實現。
通過定義繫結器Binder作為中間層,實現了應用程式與訊息中介軟體細節之間的隔離。
Binder
在沒有繫結器這個概念的情況下,我們的SpringBoot應用要 直接與訊息中介軟體進行資訊互動的時候,由於各訊息中介軟體構建的初衷不同,它們的實現細節上會有較大的差異性.通過定義繫結器作為中間層,完美地實現了應用程式與訊息中介軟體細節之間的隔離。Stream對消 息中介軟體的進一步封裝可以做到程式碼層面對中介軟體的無感知,甚於動態的切換中介軟體(rabbitmq切換為kafka), 使得微服務開發的高度解耦,服務可以關注更多自己的業務流程。
通過定義繫結器Binder作為中間層,實現了應用程式與訊息中介軟體細節之間的隔離。
- INPUT對應於消費者
- OUTPUT對應於生產者
Stream中的訊息通訊方式遵循了釋出-訂閱模式
Topic主題進行廣播
- 在RabbitMQ就是Exchange
- 在Kafka中就是Topic
Spring Cloud Stream標準流程套路
Binder
很方便的連線中介軟體,遮蔽差異
Channel
通道,是佇列Queue的一種抽象,在訊息通訊系統中就是實現儲存和轉發的媒介,通過Channel對佇列進行配置
Source和Sink
簡單的可以理解為參照物件是Spring Cloud Stream 自身,從Stream釋出訊息就是輸出,接受訊息就是輸入
編碼API和常用註解
案例說明
RabbitMQ環境已經OK
工程中新建三個子模組
- cloud-stream-rabbitmq-provider8801 ,作為訊息試生產者進行發訊息模組
- cloud-stream-rabbitmq-consumer8802,作為訊息接收模組
- cloud-stream-rabbitmq-consumer8803,作為訊息接收模組
訊息驅動之生產者
新建Module
cloud-stream-rabbitmq-provider8801
POM
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--監控-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--stream rabbit -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<!--熱部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
YML
server:
port: 8801
spring:
application:
name: cloud-stream-provider
cloud:
stream:
binders: #在此處配置要繫結的rabbitmq的服務資訊
defaultRabbit: #表示定義的名稱,用於binding整合
type: rabbit #訊息元件型別
environment: #設定rabbitmq的相關環境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: #服務的整合處理
output: #這個名字是一個通道的名稱
destination: studyExchange #表示要使用的Exchange名稱定義
content-type: application/json #設定訊息型別,本次為json,本文要設定為“text/plain”
binder: defaultRabbit #設定要繫結的訊息服務的具體設定
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 #設定心跳的時間間隔(預設是30S)
lease-expiration-duration-in-seconds: 5 #如果超過5S間隔就登出節點 預設是90s
instance-id: send-8801.com #在資訊列表時顯示主機名稱
prefer-ip-address: true #訪問的路徑變為IP地址
主啟動類StreamMQMain8801
@SpringBootApplication
public class StreamMQMain8801 {
public static void main(String[] args) {
SpringApplication.run(StreamMQMain8801.class,args);
}
}
業務類
傳送訊息介面
public interface IMessageProvider {
public String send();
}
傳送訊息介面實現類
package com.eiletxie.springcloud.service.impl;
import com.eiletxie.springcloud.service.IMessageProvider;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.MessageChannel;
import javax.annotation.Resource;
import java.util.UUID;
/**
* @Author EiletXie
* @Since 2020/3/14 14:13
*/
@EnableBinding(Source.class) //定義訊息的推送管道
public class MessageProviderImpl implements IMessageProvider {
@Resource
private MessageChannel output; // 訊息傳送管道
@Override
public String send() {
String serial = UUID.randomUUID().toString();
output.send(MessageBuilder.withPayload(serial).build());
System.out.println("*****serial: " +serial);
return null;
}
}
Controller
@RestController
public class SendMessageController {
@Resource
private IMessageProvider messageProvider;
@GetMapping("/sendMessage")
public String sendMessage() {
return messageProvider.send();
}
}
測試
啟動7001eureka
啟動rabbitmq
- rabbitmq-plugins enbale rabbitmq_management
- http://localhost:15672/
啟動8801
訪問:http://localhost:8801/sendMessage
訊息驅動之消費者
新建Module
cloud-stream-rabbitmq-consumer8802
POM
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--監控-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--stream rabbit -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<!--熱部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
YML
server:
port: 8802
spring:
application:
name: cloud-stream-consumer
cloud:
stream:
binders: #在此處配置要繫結的rabbitmq的服務資訊
defaultRabbit: #表示定義的名稱,用於binding整合
type: rabbit #訊息元件型別
environment: #設定rabbitmq的相關環境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: #服務的整合處理
input: #這個名字是一個通道的名稱
destination: studyExchange #表示要使用的Exchange名稱定義
content-type: application/json #設定訊息型別,本次為json,本文要設定為“text/plain”
binder: defaultRabbit #設定要繫結的訊息服務的具體設定
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 #設定心跳的時間間隔(預設是30S)
lease-expiration-duration-in-seconds: 5 #如果超過5S間隔就登出節點 預設是90s
instance-id: receive-8802.com #在資訊列表時顯示主機名稱
prefer-ip-address: true #訪問的路徑變為IP地址
主啟動類
@SpringBootApplication
public class StreamMQMain8802 {
public static void main(String[] args) {
SpringApplication.run(StreamMQMain8802.class,args);
}
}
業務類
@Component
@EnableBinding(Sink.class)
public class ReceiverMessageListenerController {
@Value("${server.port}")
private String serverPort;
@StreamListener(Sink.INPUT)
public void input(Message<String> message) {
System.out.println("消費者1號, -----> 接受到的訊息: " + message.getPayload()
+ "\t port: " + serverPort);
}
}
分組消費與持久化
依照8802,clone出來一份8803
啟動
RabbitMQ
7001服務註冊
8801訊息生產
8802訊息消費
8803訊息消費
執行後有兩個問題
- 有重複消費問題
- 訊息持久化問題
消費
目前是8802/8803同時收到了,存在重複消費問題
http://localhost:8801/sendMessage
如何解決
分組和持久化屬性group(重要)
生產實際案例
比如在如下場景中,訂單系統我們做叢集部署,都會從RabbitMQ中獲取訂單資訊,那如果一個訂單同時被兩個服務獲取到,那麼就會造成資料錯誤,我們得避免這種情況。
這時我們就可以使用Stream中的訊息分組來解決
注意在Stream中處於同一個group中的多個消費者競爭關係,就能夠保證訊息只會被其中一個應用消費一次。
不同組是可以全面消費的(重複消費) ,
同一組內會發生競爭關係,只有其中一個可以消費
分組
原理
微服務應用放置於同一個group中,就能夠保證訊息只會被其中一個應用消費一次。不同的組是可以消費的,同一個組內會發生競爭關係,只有其中一個可以消費
8802/8803都變成不同組,group兩個不同
group:atguiguA,atguiguB
8802修改YML
bindings: #服務的整合處理
input: #這個名字是一個通道的名稱
destination: studyExchange #表示要使用的Exchange名稱定義
content-type: application/json #設定訊息型別,本次為json,本文要設定為“text/plain”
binder: defaultRabbit #設定要繫結的訊息服務的具體設定
group: atguiguA
在bindings下新增group屬性即可
8803修改YML
bindings: #服務的整合處理
input: #這個名字是一個通道的名稱
destination: studyExchange #表示要使用的Exchange名稱定義
content-type: application/json #設定訊息型別,本次為json,本文要設定為“text/plain”
binder: defaultRabbit #設定要繫結的訊息服務的具體設定
group: atguiguB
我們自己配置
分散式微服務應用為了實現高可用和負載均衡,實際上都會部署多個例項,本例我啟動了兩個消費微服務(8802/8803)
多數情況,生產者傳送訊息給某個具體微服務時只希望被消費一次, 按照上面我們啟動兩個應用的例子,雖然它們同屬一個應用,但是這個訊息出現了被重複消費兩次的情況。為了解決這個問題,在Spring Cloud Stream中提供了消費組的概念。
結論
8802/8803實現了輪詢分組,每次只有一個消費者,8801模組的發的訊息只能被8802或8803其中一個接收到,這樣避免了重複消費
8802/8803都變成相同組,group兩個相同
group:atguiguA
8802修改YML
8803修改YML
結論
同一個組的多個微服務例項,每次只會有一個拿到
持久化
- 通過上述,解決了重複消費問題,再看看持久化
- 停止8802/8803並去除掉8802分組group:atguiguA
8803的分組group:atguiguA沒有去掉 - 8801先發送4條訊息到rabbitmq,再試著啟動8802、8803看他們是否能接收到傳送過的訊息
- 先啟動8802,無分組屬性配置,後臺沒有打出來訊息
再啟動8803,有分組屬性配置,後臺打出來了MQ上的訊息
我們發現有指定分組的服務8803,訊息可以持久化,即使服務中途斷開後重啟仍然可以獲得,而未指定分組的服務就會丟失斷開期間傳送到MQ的訊息。
理解:因為訊息傳送時,會將訊息發給各個分組,雖然8803程式已經停止了,但是它的分組B此時還是存在的,所以訊息直接發給了分組B;(其實分組A也能收到訊息)
而8802程式此時沒有分組,也就是說,當8802啟動時才給它分配預設分組,而此時新建的預設分組是在傳送訊息之後新建的,所以錯過了此訊息。