Spring Cloud Stream 是一個用來爲微服務應用構建消息驅動能力的框架。
Spring Cloud Stream 爲一些供應商的消息中間件產品提供了個性化的自動化配置實現,並引入了發佈-訂閱、消費組、分區這三個核心概念。
通過使用 Spring Cloud Stream,可以有效簡化開發人員對消息中間件的使用複雜度,讓系統開發人員可以有更多的精力關注於核心業務邏輯的處理。
目前僅支持 RabbitMQ 和 Kafka 的自動化配置。
概括:屏蔽底層消息中間件的差異,降低切換版本,統一消息的編程模型
Spring Cloud Stream中文指導手冊:https://m.wang1314.com/doc/webapp/topic/20971999.html
一、設計思想
1.標準MQ
生產者/消費者之間靠消息媒介傳遞信息內容:Message
消息必須走特定的通道:MessageChannel
消息通道里的消息如何被消費呢,誰負責收發處理:消息通道MessageChannel的子接口SubscribableChannel,由MessageHandler消息處理器訂閱
2.通過定義綁定器Binder作爲中間層,實現了應用程序與消息中間件細節之間的隔離。
INPUT對應於消費者
OUTPUT對應於生產者
3.Stream中的消息通信方式遵循了發佈-訂閱模式:Topic主題進行廣播
在RabbitMQ就是Exchange
在kafka中就是Topic
二、Spring Cloud Stream標準流程套路
1.Binder:很方便的連接中間件,屏蔽差異
2.Channel:通道,是隊列Queue的一種抽象,在消息通訊系統中就是實現存儲和轉發的媒介,通過對Channel對隊列進行配置
3.Source和Sink:簡單的可理解爲參照對象是Spring Cloud Stream自身,從Stream發佈消息就是輸出,接收消息就是輸入
Middleware:中間件,目前只支持RabbitMQ和Kafka
Binder:BInder是應用與消息中間件之間的封裝,目前實行了Kafka和RabbitMQ的Binder。
通過Binder可以方便的連接中間件,可以動態改變消息類型(對應於Kafka的topic,RabbitMQ的exchange),這些都可以通過配置文件來實現。
@Input:註解標識輸入通道,通過該輸入通道接收到的消息進入應用程序
@Output:註解標識輸出通道,發佈的消息將通過該通道離開應用程序
@StreamListener:監聽隊列,用於消費者的隊列的消息接收
@EnableBinding:指信道channel和exchange綁定在一起
三、案例
1.rabbitMQ開啓
2.生產者模塊,進行發消息:stream-rabbitmq-provider8801
1)新建module:stream-rabbitmq-provider8801
2)pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>demo2020</artifactId>
<groupId>cn.chen.demo</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>stream-rabbitmq-provider8801</artifactId>
<dependencies>
<!--stream-rabbit-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<!-- eureka-client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--公共模塊-->
<dependency>
<groupId>cn.chen.demo</groupId>
<artifactId>api-common</artifactId>
<version>${project.version}</version>
</dependency>
<!--基本配置-->
<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>
<!-- devtools工具 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
3)yml
server:
port: 8801
spring:
application:
name: 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: # 客戶端進行Eureka註冊的配置
service-url:
defaultZone: http://eureka7001.com:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 # 設置心跳的時間間隔(默認是30秒)
lease-expiration-duration-in-seconds: 5 # 如果現在超過了5秒的間隔(默認是90秒)
instance-id: send-8801.com # 在信息列表時顯示主機名稱
prefer-ip-address: true # 訪問的路徑變爲IP地址
4)主啓動
@SpringBootApplication
public class StreamMQProvider8801 {
public static void main(String[] args) {
SpringApplication.run(StreamMQProvider8801.class, args);
}
}
5)業務類
- 發送消息接口
public interface IMessageProvider {
/**
* @title send
* @Description 發送消息
**/
public void send();
}
- 發送消息接口實現類
@Slf4j
@EnableBinding(Source.class) //定義消息的推送管道
public class MessageProviderImpl implements IMessageProvider{
@Autowired
private MessageChannel output; // 消息發送管道
@Override
public void send() {
String serial = UUID.randomUUID().toString();
output.send(MessageBuilder.withPayload(serial).build());
log.info("隨機數:" + serial);
}
}
- controller
@RestController
@RequestMapping("/send")
public class SendMessageController {
@Resource
private IMessageProvider messageProvider;
@GetMapping(value = "/sendMessage")
public String sendMessage()
{
return messageProvider.send();
}
}
3.消費者模塊,接收消息:stream-rabbitmq-consumer8802
1)新建module:stream-rabbitmq-consumer8802
2)pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>demo2020</artifactId>
<groupId>cn.chen.demo</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>stream-rabbitmq-consumer8802</artifactId>
<dependencies>
<!--stream-rabbit-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<!-- eureka-client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--公共模塊-->
<dependency>
<groupId>cn.chen.demo</groupId>
<artifactId>api-common</artifactId>
<version>${project.version}</version>
</dependency>
<!--基本配置-->
<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>
<!-- devtools工具 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
3)yml
server:
port: 8802
spring:
application:
name: 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: # 客戶端進行Eureka註冊的配置
service-url:
defaultZone: http://eureka7001.com:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 # 設置心跳的時間間隔(默認是30秒)
lease-expiration-duration-in-seconds: 5 # 如果現在超過了5秒的間隔(默認是90秒)
instance-id: receiver-8802.com # 在信息列表時顯示主機名稱
prefer-ip-address: true # 訪問的路徑變爲IP地址
4)主啓動
@SpringBootApplication
public class StreamMQConsumer8802 {
public static void main(String[] args) {
SpringApplication.run(StreamMQConsumer8802.class, args);
}
}
5)業務類
@Component
@EnableBinding(Sink.class)
@Slf4j
public class ReceiverMessageListenerController {
@Value("${server.port}")
private String serverPort;
@StreamListener(Sink.INPUT)
public void input(Message<String> message) {
log.info("消費者1號,接受:"+message.getPayload()+"\t port:"+serverPort);
}
}
4.消費者模塊,接收消息:stream-rabbitmq-consumer8803
同上copy一份修改即可
5.分組消費與持久化
1)啓動兩個消費者之後出現了問題:
- 有重複消費問題
- 消息持久化問題
2)分組:解決重複消費問題
-
原理:微服務應用放置於同一個group中,就能夠保證消息只會被其中一個應用消費一次。不同的組是可以消費的,同一個組內會發生競爭關係,只有其中一個可以消費。
-
8802、8803不同組情況:yml添加分組配置
8802:consumer1
8803:consumer2
server:
port: 8802
spring:
application:
name: 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 # 設置要綁定的消息服務的具體設置
group: consumer1 # 消費者分組
eureka:
client: # 客戶端進行Eureka註冊的配置
service-url:
defaultZone: http://eureka7001.com:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 # 設置心跳的時間間隔(默認是30秒)
lease-expiration-duration-in-seconds: 5 # 如果現在超過了5秒的間隔(默認是90秒)
instance-id: receiver-8802.com # 在信息列表時顯示主機名稱
prefer-ip-address: true
結論:重複消費
- 8802、8803同組情況:group: consumer
8802/8803實現了輪詢分組,每次只有一個消費者 8801模塊發的消息只能被8802或8803其中一個接收到,這樣避免了重複消費
結論:同一個組的多個微服務實例,每次只會有一個拿到
3)持久化