一、消息驅動概述
1.1 什麼是SpringCloud Stream?
屏蔽底層消息中間件的差異,降低切換版本,統一消息的編程模型。
官方定義Spring Cloud Stream是一個構建消息驅動微服務的框架。
應用程序通過inputs或者outputs來與Spring Cloud Stream中的binder對象交互。通過我們配置來bingding(綁定),而Spring Cloud Stream的binder對象負責與消息中間件交互。所以,我們只需要搞清楚如何與Spring Cloud Stream交互就可以方便使用消息驅動的方式。
通過使用Spring Integration來連接消息代理中間件以實現消息事件驅動。Spring Cloud Stream爲一些供應商的消息中間件產品提供了個性化的自動化配置實現,引用了發佈-訂閱、消費組、分區的三個核心概念。目前僅支持RabbitMQ、Kafka。
1.2 官網
1.2.1 https://spring.io/projects/spring-cloud-stream#overview
https://cloud.spring.io/spring-cloud-static/spring-cloud-stream/3.0.1.RELEASE/reference/html/
1.2.2 Spring Cloud Stream中文指導手冊
https://m.wang1314.com/doc/webapp/topic/20971999.html
1.3 設計思想
1.3.1 標準MQ
生產者、消費者之間靠消息媒介傳遞信息內容-Message
消息必須走特定的通道-消息通道MessageChannel
消息通道里的消息如何被消費呢,誰負責收發處理-消息通道MessageChannel的子接口SubScribableChannel,由MessageHandler消息處理器訂閱。
1.3.2 爲什麼用Cloud Stream
比方說我們用到了RabbitMQ和Kafka,由於這兩個消息中間件的架構上的不同,像RabbitMQ有exchange,kafka有Topic和Partitions分區。
這些中間件的差異性導致我們實際項目開發給我們造成了一定的困擾,我們如果用了兩個消息隊列的其中一種,後面的業務需求,我們想往另外一種消息隊列進行遷移,這時候無疑就是一個災難性的,一大堆東西都要重新推倒重新做,因爲它跟我們的系統耦合了,這時候SpringCloud Stream給我們提供了一種解耦合的方式。
1.3.3 SpringCloud Stream憑什麼可以統一底層差異
在沒有綁定器這個概念的情況下,我們的SpringBoot應用要直接與消息中間件進行信息交互的時候,由於各消息中間件構建的初衷不同,它們的實現細節上會有較大的差異性。通過定義綁定器作爲中間層,完美地實現了應用程序與消息中間件細節之間的隔離。通過嚮應用程序暴露統一的Channel通道,使得應用程序不需要再考慮各種不同的消息中間件實現。
通過定義綁定器Binder作爲中間層,實現了應用程序與消息中間件細節之間隔離。
1.3.4 Binder
在沒有綁定器這個概念的情況下,我們的SpringBoot應用要直接與消息中間件進行信息交互的時候,由於各消息中間件構建的初衷不同,它們的實現細節上會有較大的差異性。通過定義綁定器作爲中間層,完美地實現了應用程序與消息中間件細節之間的隔離。Stream對消息中間件的進一步封裝,可以做到代碼層面對中間件的無感知,甚至於動態的切換中間件(rabbitmq切換爲kafka),使得微服務開發的高度解耦,服務可以關注更多自己的業務流程。
input對應於消費者
output對應於生產者
1.3.5 Stream中的消息通信方式遵循了發佈-訂閱模式
Topic主題進行廣播
在RabbitMQ中就是Exchange
在Kafka中就是Topic
1.4 Spring Cloud Stream標準流程套路
1.4.1 Binder
很方便的連接中間件,屏蔽差異
1.4.2 Channel
通道,是隊列Queue的一種抽象,在消息通訊系統中就是實現存儲和轉發的媒介,通過對Channel對隊列進行配置。
1.4.3 Source和Sink
簡單的可理解爲參照對象是Spring Cloud Stream自身,從Stream發佈消息就是輸出,接受消息就是輸入。
1.5 編碼API和常用註解
二、案例說明
RabbitMQ環境已經OK
工程中新建三個子模塊
cloud-stream-rabbitmq-provider8801,作爲生產者進行發消息模塊
cloud-stream-rabbitmq-consumer8802,作爲消息接收模塊
cloud-stream-rabbitmq-consumer8803,作爲消息接收模塊
三、 消息驅動之生產者
3.1 新建Module
cloud-stream-rabbitmq-provider8801
3.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>cloud2020</artifactId>
<groupId>com.atguigu.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-stream-rabbitmq-provider8801</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-eureka-server -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-devtools -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
3.3 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”
default-binder: defaultRabbit # 設置要綁定的消息服務的具體設置default-binder
eureka:
client: # 客戶端進行Eureka註冊的配置
service-url:
defaultZone: http://localhost: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地址
3.4 主啓動類StreamMQMain8801
package com.atguigu.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author by XXX
* @descriptaion #
* @date 2020/6/30
*/
@SpringBootApplication
public class StreamMQMain8801 {
public static void main(String[] args) {
SpringApplication.run(StreamMQMain8801.class, args);
}
}
3.5 業務類
發送消息接口
package com.atguigu.springcloud.service;
public interface IMessageProvider
{
public String send();
}
發送消息接口實現類
package com.atguigu.springcloud.service.impl;
import com.atguigu.springcloud.service.IMessageProvider;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.integration.support.MessageBuilderFactory;
import org.springframework.messaging.MessageChannel;
import org.springframework.integration.support.MessageBuilder;
import javax.annotation.Resource;
import org.springframework.cloud.stream.messaging.Source;
import javax.annotation.Resource;
import java.util.UUID;
@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
package com.atguigu.springcloud.controller;
import com.atguigu.springcloud.service.IMessageProvider;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class SendMessageController
{
@Resource
private IMessageProvider messageProvider;
@GetMapping(value = "/sendMessage")
public String sendMessage()
{
return messageProvider.send();
}
}
3.6 測試
- 啓動7001eureka
- 啓動rabbitmq
- rabbitmq-plugins enable rabbitmq_management
- 啓動8801
訪問http://localhost:8801/sendMessage
四、消息驅動之消費者
4.1 新建Module
cloud-stream-rabbitmq-consumer8802
4.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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud2020</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>cloud-provider-payment8001</module>
<module>cloud-consumer-order80</module>
<module>cloud-api-commons</module>
<module>cloud-eureka-server7001</module>
<module>cloud-eureka-server7002</module>
<module>cloud-provider-payment8002</module>
<module>cloud-consumer-feign-order80</module>
<module>cloud-provider-hystrix-payment8001</module>
<module>cloud-consumer-feign-hystrix-order80</module>
<module>cloud-consumer-hystrix-dashboard9001</module>
<module>cloud-gateway-gateway9527</module>
<module>cloud-config-center-3344</module>
<module>cloud-config-client-3355</module>
<module>cloud-config-client-3366</module>
<module>cloud-stream-rabbitmq-provider8801</module>
<module>cloud-stream-rabbitmq-consumer8802</module>
</modules>
<!-- 統一管理jar包版本 -->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>4.12</junit.version>
<log4j.version>1.2.17</log4j.version>
<lombok.version>1.16.18</lombok.version>
<mysql.version>5.1.47</mysql.version>
<druid.version>1.1.16</druid.version>
<mybatis.spring.boot.version>1.3.0</mybatis.spring.boot.version>
</properties>
<!-- 子模塊繼承之後,提供作用:鎖定版本+子modlue不用寫groupId和version -->
<dependencyManagement>
<dependencies>
<!--spring boot 2.2.2-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.2.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--spring cloud Hoxton.SR1-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--spring cloud alibaba 2.1.0.RELEASE-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.spring.boot.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
<addResources>true</addResources>
</configuration>
</plugin>
</plugins>
</build>
</project>
4.3 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”
default-binder: defaultRabbit # 設置要綁定的消息服務的具體設置default-binder
eureka:
client: # 客戶端進行Eureka註冊的配置
service-url:
defaultZone: http://localhost:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 # 設置心跳的時間間隔(默認是30秒)
lease-expiration-duration-in-seconds: 5 # 如果現在超過了5秒的間隔(默認是90秒)
instance-id: receive-8802.com # 在信息列表時顯示主機名稱
prefer-ip-address: true # 訪問的路徑變爲IP地址
4.4 主啓動類StreamMQMain8802
package com.atguigu.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author by Jak
* @date 2020/6/30
*/
@SpringBootApplication
public class StreamMQMain8802 {
public static void main(String[] args) {
SpringApplication.run(StreamMQMain8802.class, args);
}
}
4.5 業務類
package com.atguigu.springcloud.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;
/**
* @author by Jak
* @date 2020/6/30
*/
@Component
@EnableBinding(Sink.class)
public class ReceiveMessageListenerController {
@Value("${server.port}")
private String serverPort;
@StreamListener(Sink.INPUT)
public void input(Message<String> message) {
System.out.println("消費者1號,----->接收到的消息: " + message.getPayload() + "\t port: " + serverPort);
}
}
4.6 測試
測試8801發送8802接收消息
http://localhost:8801/sendMessage
- 啓動7001
- 啓動8801
- 啓動8802
- 啓動rabbitmq
rabbitmq-plugins enable rabbitmq_management
http://localhost:8801/sendMessage
8802接收到消息
五、分組消費與持久化
5.1 新建cloud-stream-rabbitmq-consumer8803
5.1.1 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>cloud2020</artifactId>
<groupId>com.atguigu.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-stream-rabbitmq-consumer8803</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</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>
</project>
5.1.2 YML
server:
port: 8803
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”
defaultbinder: defaultRabbit # 設置要綁定的消息服務的具體設置
group: atguiguA
eureka:
client: # 客戶端進行Eureka註冊的配置
service-url:
defaultZone: http://localhost:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 # 設置心跳的時間間隔(默認是30秒)
lease-expiration-duration-in-seconds: 5 # 如果現在超過了5秒的間隔(默認是90秒)
instance-id: receive-8803.com # 在信息列表時顯示主機名稱
prefer-ip-address: true # 訪問的路徑變爲IP地址
5.1.3 主啓動類
package com.atguigu.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class StreamMQMain8803
{
public static void main(String[] args)
{
SpringApplication.run(StreamMQMain8803.class,args);
}
}
5.1.4 業務類
package com.atguigu.springcloud.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;
@Component
@EnableBinding(Sink.class)
public class ReceiveMessageListenerController
{
@Value("${server.port}")
private String serverPort;
@StreamListener(Sink.INPUT)
public void input(Message<String> message)
{
System.out.println("消費者2號,----->接受到的消息: "+message.getPayload()+"\t port: "+serverPort);
}
}
5.2 啓動
http://localhost:8801/sendMessage發送兩條消息
8802,8803收到同樣的消息
5.3 啓動後兩個問題
- 有重複消費問題
- 消息持久化問題
5.3.1 消費
目前是8802/8803同時都收到了,存在重複消費問題
如何解決
分組和持久化屬性group
生產實際案例
5.4 分組
5.4.1 原理
微服務應用放置於同一個group中,就能夠保證消息只會被其中一個應用消費一次。不同的組是可以重複消費,同一個組內會發生競爭關係,只有其中一個可以消費。
5.4.2 8802/8803都變成不同組,group兩個不同
group:attuiguA、atguiguB
8802修改YML
8803修改YML
我們自己的配置
結論
不同的組,還是重複消費
8802/8803實現了輪詢分組,每次只有一個消費者,8801模塊發的消息只能被8802或8803其中一個接收到,這樣避免了重複消費
8802/8803都變成相同組,group兩個相同
group:atguiguA
8802/8803修改YML
發送方發送兩條消息
接收方8802/8803各有一條消息,避免了重複消費
結論
同一個組的多個微服務實例,每次只會有一個拿到
5.5 持久化
- 停止8802/8803並去除掉8802的分組group:atguiguA,8803的分組group:atguiguA沒有去掉
- 8801先發送4條消息到rabbitmq
- 先啓動8802,去掉了分支atguiguA,無分組屬性配置,後臺沒有打出來消息
- 再啓動8803,有分組屬性配置,後臺打出來了MQ上的消息