背景及痛點(diǎn)
現(xiàn)如今消息中間件(MQ)在互聯(lián)網(wǎng)項(xiàng)目中被廣泛的應(yīng)用,特別是大數(shù)據(jù)行業(yè)應(yīng)用的特別的多,現(xiàn)在市面上也流行這多個(gè)消息中間件框架,比如ActiveMQ
、RabbitMQ
、RocketMQ
、Kafka
等,這些消息中間件各有各的優(yōu)劣,但是想要解決的問題都基本相同。由于每個(gè)框架都有它自己的使用方式,這無疑是增加了開發(fā)者的學(xué)習(xí)成本以及添加相同的業(yè)務(wù)復(fù)雜度。框架的變更或者多個(gè)中間件的混合使用使得業(yè)務(wù)邏輯代碼中中間件的切換、項(xiàng)目的維護(hù)和開發(fā)都會(huì)變得更加繁瑣。
有沒有一種技術(shù)讓我們不再需要關(guān)注具體MQ的使用細(xì)節(jié),我們只需要專注業(yè)務(wù)邏輯的開發(fā),讓程序根據(jù)實(shí)際項(xiàng)目的使用自己去適配綁定,自動(dòng)在各種MQ內(nèi)切換呢?springcloud stream
便為此而生。
關(guān)于stream
我們用一句話來描述stream就是:屏蔽底層消息中間件的差異,降低切換版本,統(tǒng)一消息的編程模型
官方定義SpringCloud Stream
是一個(gè)構(gòu)建消息驅(qū)動(dòng)微服務(wù)的框架,應(yīng)用通過inputs
或者outputs
來與SpringCloud Stream
中的binder
對(duì)象交互,我們通過配置來綁定消息中間件,而SpringCloud Stream
的binder
對(duì)象負(fù)責(zé)與消息中間件交互,所以我們只需要搞清楚如何與SpringCloud Stream
交互即可方便的使用消息中間件。
SpringCloud Stream
通過Spring Integration
來連接消息代理中間件以實(shí)現(xiàn)消息事件驅(qū)動(dòng),它提供了個(gè)性化的自動(dòng)化配置,引用了發(fā)布訂閱
、消費(fèi)組
、分區(qū)
的三個(gè)核心概念,但是目前僅支持RabbitMQ
和Kafka
設(shè)計(jì)思想
在此之前
生產(chǎn)者和消費(fèi)者通過消息媒介(queue等)傳遞信息內(nèi)容(Message),消息必須通過特定的通道(MessageChannel),通過消息的發(fā)布與訂閱來決定消息的發(fā)送和消費(fèi)(publish/subscrib
)。
引入中間件
現(xiàn)在假如我們用到了RabbitMQ
和Kafka
,由于這兩個(gè)消息中間件的架構(gòu)上的不同,像RabbitMQ
有Exchange
,而Kafka
有topiche
和Partitions
分區(qū)
(binder中,input對(duì)于消費(fèi)者,output對(duì)應(yīng)生產(chǎn)者。)
這些中間件的差異性導(dǎo)致我們實(shí)際項(xiàng)目開發(fā)給我們造成了一定的困擾,我們?nèi)绻昧藘蓚€(gè)消息隊(duì)列的其中一種,但是后面因?yàn)闃I(yè)務(wù)需求,需要改用另外一種消息隊(duì)列進(jìn)行遷移,這時(shí)候無疑就是一 個(gè)災(zāi)難性的,一大堆東西都要重新推倒重新做,因?yàn)樗覀兊南到y(tǒng)耦合了,這時(shí)候springcloud Stream
給我們提供了一種解耦合的方式。
屏蔽底層差異
在沒有綁定器(Builder
)這個(gè)概念的情況下,我們的SpringBoot應(yīng)用要直接與消息中間件進(jìn)行信息交互的時(shí)候,由于各消息中間件構(gòu)建的初衷不同,它們的實(shí)現(xiàn)細(xì)節(jié)上會(huì)有較大的差異性,通過定義綁定器作為中間件,完美地實(shí)現(xiàn)了應(yīng)用程序與消息中間件細(xì)節(jié)之間的隔離。通過向應(yīng)用程序暴露統(tǒng)一的Channel通道, 使得應(yīng)用程序不需要再考慮各種不同的消息中間件實(shí)現(xiàn)。
通過定義綁定器Binder作為中間層,實(shí)現(xiàn)了應(yīng)用程序與消息中間件細(xì)節(jié)之間的隔離。
處理架構(gòu)
Stream對(duì)消息中間件的進(jìn)一步封裝可以做到代碼層面對(duì)中間件的無感知,甚至于動(dòng)態(tài)的切換中間件(rabbitmq切換為kafka),使得微服務(wù)開發(fā)的高度解耦,服務(wù)可以關(guān)注更多自己的業(yè)務(wù)流程。
其遵循了發(fā)布-訂閱模式,主要使用的就是Topic主題進(jìn)行廣播,RabbitMQ就是Exchange,在Kafka中就是Topic
通過定義綁定器Binder作為中間層,實(shí)現(xiàn)了應(yīng)用程序與消息中間件細(xì)節(jié)之間的隔離。
stream流程
-
Binder
:很方便的連接中間件,屏蔽差異 -
Channel
:通道是隊(duì)列Queue的一種抽象,在消息通訊系統(tǒng)中就是實(shí)現(xiàn)存儲(chǔ)和轉(zhuǎn)發(fā)的媒介,通過對(duì)Channel對(duì)隊(duì)列進(jìn)行配置 -
Source和Sink
:簡單的可理解為參照對(duì)象是Spring Cloud Stream自身,從Stream發(fā)布消息就是輸出,接受消息就是輸入
常用api和注解
使用示例
基本環(huán)境
注冊中心
:Eureka,可以是其他。-
消息中間件
:RabbitMQrabbitmq: host: localhost port: 5672 username: guest password: guest
生產(chǎn)端
依賴
<!--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>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
配置文件
server:
port: 8801
spring:
application:
name: cloud-stream-provider
cloud:
# stream 配置
stream:
binders: # 配置綁定的消息中間件的服務(wù)信息
defaultRabbit: # 自定義的一個(gè)名稱,用來下面 bindings 綁定
type: rabbit # 消息組件的類型
environment: #相關(guān)環(huán)境配置,設(shè)置rabbitmq的環(huán)境
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: # 服務(wù)的整合處理
output: # 通道名稱
destination: testExchange # 定義要使用的Exchange的名稱
content-type: application/json # 設(shè)置消息類型,對(duì)象為json,文本是text/plain
binder: defaultRabbit # 設(shè)置要綁定的服務(wù)的具體設(shè)置,就是我們上面配置的defaultRabbit
eureka:
client:
#表示是否將自己注冊進(jìn)EurekaServer默認(rèn)為true
register-with-eureka: true
#是否從EurekaServer抓取已有的注冊消息,默認(rèn)為true,單節(jié)點(diǎn)無所謂,集群必須設(shè)置為true才能配合ribbon使用負(fù)載均衡
fetch-registry: true
service-url:
#單機(jī)版
defaultZone: http://localhost:8080/eureka/
instance:
prefer-ip-address: true
instance-id: sender01
定義接口
這里需要定義一個(gè)接口并實(shí)現(xiàn)它,方便其他業(yè)務(wù)調(diào)用。
public interface IMessageProvider {
/**
* 發(fā)送接口
* @param msg
* @return
*/
public String send(String msg);
}
接口實(shí)現(xiàn)
接口實(shí)現(xiàn)中需要添加
@EnableBinding
注解,并引入Source.class
,為什么引入Source.class
呢?因?yàn)樗巧a(chǎn)者,我們參考stream流程圖就可以知道
import com.martain.study.service.IMessageProvider;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.MessageBuilder;
import javax.annotation.Resource;
@EnableBinding(Source.class)
public class MessageProvider implements IMessageProvider {
/**
* 注入消息發(fā)送管道
*/
@Resource
private MessageChannel output;
@Override
public String send(String msg) {
output.send(MessageBuilder.withPayload(msg).build());
System.out.println("******send message:"+msg);
return msg;
}
}
定義測試controller
@RestController
public class TestController {
@Autowired
IMessageProvider messageProvider;
@GetMapping("/sendMsg")
public String sendMsg(){
String msg = UUID.randomUUID().toString();
return messageProvider.send(msg);
}
}
啟動(dòng)類
@SpringBootApplication
public class StreamProviderApplication8801 {
public static void main(String[] args) {
SpringApplication.run(StreamProviderApplication8801.class,args);
}
}
服務(wù)啟動(dòng)之后,多次請求/sendMsg
,發(fā)送了多條消息。
消費(fèi)端
依賴
<!--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>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
配置文件
與生產(chǎn)者類似,只是bindings中的output改成了input
server:
port: 8802
spring:
application:
name: cloud-stream-consume
cloud:
# stream 配置
stream:
binders: # 配置綁定的消息中間件的服務(wù)信息
defaultRabbit: # 自定義的一個(gè)名稱,用來下面 bindings 綁定
type: rabbit # 消息組件的類型
environment: #相關(guān)環(huán)境配置,設(shè)置rabbitmq的環(huán)境
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: # 服務(wù)的整合處理
input: # 通道名稱
destination: testExchange # 定義要使用的Exchange的名稱
content-type: application/json # 設(shè)置消息類型,對(duì)象為json,文本是text/plain
binder: defaultRabbit # 設(shè)置要綁定的服務(wù)的具體設(shè)置,就是我們上面配置的defaultRabbit
eureka:
client:
#表示是否將自己注冊進(jìn)EurekaServer默認(rèn)為true
register-with-eureka: true
#是否從EurekaServer抓取已有的注冊消息,默認(rèn)為true,單節(jié)點(diǎn)無所謂,集群必須設(shè)置為true才能配合ribbon使用負(fù)載均衡
fetch-registry: true
service-url:
#單機(jī)版
defaultZone: http://localhost:8080/eureka/
instance:
prefer-ip-address: true
instance-id: recover01
接收服務(wù)
接收服務(wù)只需要再類名前添加
@EnableBinding()
注解,并引入Sink.class
類,而實(shí)際接收的方法中需要添加@StreamListener(Sink.INPUT)
注解。
package com.martain.study.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 {
/**
* 獲取本服務(wù)的端口
*/
@Value("${server.port}")
private String serverPort;
/**
* 這里表示監(jiān)聽sink的input
* @param message
*/
@StreamListener(Sink.INPUT)
public void input(Message<String> message){
System.out.println("**** recv msg :"+message.getPayload()+" in port "+serverPort);
}
}
啟動(dòng)類
@SpringBootApplication
public class StreamConsumerApplication8802 {
public static void main(String[] args) {
SpringApplication.run(StreamConsumerApplication8802.class,args);
}
}
啟動(dòng)生產(chǎn)服務(wù)后,在啟動(dòng)消費(fèi)服務(wù),多次請求生產(chǎn)服務(wù)發(fā)送消息,我們可以發(fā)現(xiàn)消費(fèi)者能很快的消費(fèi)這些消息。
消息分組
當(dāng)我們有多個(gè)
消費(fèi)者
時(shí),這個(gè)時(shí)候生產(chǎn)者生產(chǎn)一條消息,會(huì)發(fā)現(xiàn)所有的消費(fèi)者都會(huì)消費(fèi)這個(gè)消息。比如在一些訂單系統(tǒng)的場景中,如果一個(gè)訂單被多個(gè)處理服務(wù)一起獲取到,就容易造成數(shù)據(jù)錯(cuò)誤,那我們?nèi)绾伪苊膺@種情況呢?這時(shí)我們就可以使用Stream的消息分組
來解決重復(fù)消費(fèi)問題。
如何實(shí)現(xiàn)Stream的消息分組呢?我們只要簡單的在yml文件中配置spring.cloud.stream.bindings.input.group
即可。示例如下:
...
spring:
application:
name: cloud-stream-consume
cloud:
# stream 配置
stream:
binders: # 配置綁定的消息中間件的服務(wù)信息
defaultRabbit: # 自定義的一個(gè)名稱,用來下面 bindings 綁定
type: rabbit # 消息組件的類型
environment: #相關(guān)環(huán)境配置,設(shè)置rabbitmq的環(huán)境
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: # 服務(wù)的整合處理
input: # 通道名稱
destination: testExchange # 定義要使用的Exchange的名稱
content-type: application/json # 設(shè)置消息類型,對(duì)象為json,文本是text/plain
binder: defaultRabbit # 設(shè)置要綁定的服務(wù)的具體設(shè)置,就是我們上面配置的defaultRabbit
group: groupA # 配置分組
...
如果沒有設(shè)置該屬性,當(dāng)消費(fèi)服務(wù)啟動(dòng)時(shí),會(huì)有個(gè)隨機(jī)的組名
。
如果我們將所有的消費(fèi)服務(wù)的group
熟悉都設(shè)置成一致的話,這些服務(wù)就會(huì)在同一個(gè)組里面,從而能夠保證消息只被應(yīng)用消費(fèi)一次。
同一組的消費(fèi)者是競爭關(guān)系,不可以重復(fù)消費(fèi)。
消息持久化
當(dāng)生產(chǎn)者在持續(xù)生產(chǎn)消息,消費(fèi)服務(wù)突然掛了,使得擁有許多消息并沒有被消費(fèi),如果消費(fèi)沒有配置分組的話,消費(fèi)服務(wù)重啟是無法消費(fèi)未消費(fèi)的消息的,如果配置了分組的話,當(dāng)消費(fèi)服務(wù)重啟之后可以自動(dòng)去消費(fèi)未消費(fèi)的數(shù)據(jù)。