SpringCloud(七)Stream訊息驅動

Odousang發表於2021-05-05

Stream訊息驅動

概述

遮蔽底層訊息中介軟體的差異,降低切換成本,統一訊息的程式設計模型
官網:https://cloud.spring.io/spring-cloud-static/spring-cloud-stream/3.0.4.RELEASE/reference/html/

官方定義Spring Cloud Stream是一個構建訊息驅動微服務的框架
應用程式通過 inputs 或者 outputs 來與Spring Cloud Stream中的 binder物件互動,通過配置來binding(繫結),而 Spring Cloud Stream 的 binder 物件負責與訊息中介軟體互動,所以,我們只需要搞清楚如何與Spring Cloud Stream互動就可以方便使用訊息驅動的方式
通過使用Spring Integration來連線訊息代理中介軟體以實現訊息事件驅動
Spring Cloud Stream是用於構建與共享訊息傳遞系統連線的高度可伸縮的事件驅動微服務框架,該框架提供了一個靈活的程式設計模型,它建立在已建立和熟悉的Spring和最佳實踐上,包括支援持久化的釋出/訂閱、消費組以及訊息分割槽這三個核心概念

目前僅支援RabbitMQ、Kafka

設計思想

標準MQ

生產者/消費者之間靠訊息媒介(Message)傳遞資訊內容
訊息必須走特定的通道(訊息通道MessageChannel)
訊息通道里的訊息如何被消費呢,誰負責收發處理訊息通道MessageChannel的子介面SubscribableChannel,由MessageHandler訊息處理器所訂閱

使用原因

比方說我們用到了RabbitMQ和Kafka,由於這兩個訊息中介軟體的架構上的不同,像RabbitMQ有exchange,kafka有Topic和Partitions分割槽
這些中介軟體的差異性導致我們實際專案開發給我們造成了一定的困擾,我們如果用了兩個訊息佇列的其中一種,後面的業務需求,我想往另外一種訊息佇列進行遷移,這時候無疑就是一個災難性的,一大堆東西都要重新推倒重新做,因為它跟我們的系統耦合了,這時候springcloud Stream給我們提供了一種解耦合的方式
在沒有繫結器這個概念的情況下,我們的SpringBoot應用要直接與訊息中介軟體進行資訊互動的時候,由於各訊息中介軟體構建的初衷不同,它們的實現細節上會有較大的差異性
通過定義繫結器 Binder 作為中間層,實現了應用程式與訊息中介軟體細節之間的隔離
通過嚮應用程式暴露統一的 Channel 通道,使得應用程式不需要再考慮各種不同的訊息中介軟體實現
Stream中的訊息通訊方式遵循了釋出-訂閱模式,Topic主題進行廣播,在RabbitMQ就是Exchange,在Kafka中就是Topic

基本流程

  • Binder:很方便的連線中介軟體,遮蔽差異
  • Channel:通道,是佇列Queue的一種抽象,在訊息通訊系統中就是實現儲存和轉發的媒介,通過Channel對佇列進行配置
  • Source和Sink:簡單的可理解為參照物件是Spring Cloud Stream自身,從Stream釋出訊息就是輸出,接受訊息就是輸入

常用API和註解

  • Middleware:中介軟體,目前只支援RabbitMQ和Kafka
  • Binder:Binder是應用與訊息中介軟體之間的封裝,目前實行了Kafka和RabbitMQ的Binder,通過Binder可以很方便的連線中介軟體,可以動態的改變訊息型別(對應於Kafka的topic,RabbitMQ的exchange),這些都可以通過配置檔案來實現
  • @Input:註解標識輸入通道,通過該輸入通道接收到的訊息進入應用程式
  • @Output:註解標識輸出通道,釋出的訊息將通過該通道離開應用程式
  • @StreamListener:監聽佇列,用於消費者的佇列的訊息接收
  • @EnableBinding:指通道channel和exchange繫結在一起

基本構建

新建三個子模組,一個作為生產者進行傳送訊息模組,兩個作為訊息接收模組

服務端

  1. 匯入 pom 依賴
<!-- spring-cloud-starter-stream-rabbit -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
  1. 修改 yml 配置檔案
server:
  port: 8801

spring:
  application:
    name: stream-rabbitmq-provider
  cloud:
    stream:
      # 配置要繫結的 rabbitmq 的服務資訊
      binders:
        # 表示定義的名稱,用於與 binding 整合
        defaultRabbit:
          # 訊息元件型別
          type: rabbit
          # 設定 rabbitmq 相關配置環境
          enviroment:
            spring:
              rabbitmq:
                host: localhost
                port: 5672
                username: admin
                password: 123456
      # 服務整合處理
      bindings:
        # 通道名稱
        output:
          # 表示要使用的 Exchange 名稱定義
          destination: studyExchange
          # 設定訊息型別,本次為為 json,文字則設定“text/plain”
          content-type: application/json
            binder: defaultRabbit
  1. 業務類
    傳送訊息的介面
public interface MessageProvider {
    /**
     * 訊息傳送
     *  @return :返回值
     */
    Message<?> send();
}

傳送訊息介面的實現類

@EnableBinding(Source.class)
public class MessageProviderImpl implements MessageProvider {

    /**
     * @ InboundChannelAdapter
     * 作用:表示定義的方法能產生訊息
     * fixedDelay:多少毫秒傳送1次
     */
    @Override
    @InboundChannelAdapter(channel = Source.OUTPUT,poller = @Poller(fixedDelay = "10000"))  // 每隔10秒傳送一次
    public Message<String> send() {
        String serial = UUID.randomUUID().toString();
        return MessageBuilder.withPayload(serial)
                .build();
    }
}

Controller 層

@RestController
public class SendMessageController {

    @Resource
    private MessageProvider provider;

    @GetMapping(value = "/senMessage")
    public Message senMessage(){
        return provider.send();
    }
}

消費者

  1. 匯入 pom 依賴
<!-- spring-cloud-starter-stream-rabbit -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
  1. 修改 yml 配置檔案
# 服務整合處理
bindings:
  # 通道名稱
  input:
    # 表示要使用的 Exchange 名稱定義
    destination: studyExchange
    # 設定訊息型別,本次為為 json,文字則設定“text/plain”
    content-type: application/json
    binder: defaultRabbit
  1. 業務類
@RestController
public class SendMessageController {

    @Resource
    private MessageProvider provider;

    @GetMapping(value = "/senMessage")
    public Message senMessage(){
        return provider.send();
    }
}

分組消費和持久化

當使用兩個消費者來進行接收訊息時,會出現兩個問題:重複消費和訊息持久化的問題

重複消費

目前是8802/8803同時都收到了,存在重複消費問題

解決方法:分組和持久化屬性Group
比如在如下場景中,訂單系統我們做叢集部署,都會從RabbitMQ中獲取訂單資訊
那如果一個訂單同時被兩個服務獲取到,那麼就會造成資料錯誤,為了避免這種情況這時我們就可以使用Stream中的訊息分組來解決

注意在Stream中處於同一個group中的多個消費者是競爭關係,就能夠保證訊息只會被其中一個應用消費一次,不同組是可以全面消費的(重複消費),同一組內會發生競爭關係,只有其中一個可以消費

分組

微服務應用放置於同一個group中,就能夠保證訊息只會被其中一個應用消費一次不同的組是可以消費的,同一個組內會發生競爭關係,只有其中一個可以消費
自定義分組
修改消費者的 yml 檔案,新增一個 group 的屬性

# 服務整合處理
bindings:
  # 通道名稱
  input:
    # 表示要使用的 Exchange 名稱定義
    destination: studyExchange
    # 設定訊息型別,本次為為 json,文字則設定“text/plain”
    content-type: application/json
    binder: defaultRabbit
   group: Consumer

分散式微服務應用為了實現高可用和負載均衡,實際上都會部署多個例項,這裡舉例實現兩個消費微服務
多數情況,生產者傳送訊息給某個具體微服務時只希望被消費一次,按照上面啟動兩個應用的例子,雖然它們同屬一個應用,但是這個訊息出現了被重複消費兩次的情況。為了解決這個問題,在Spring Cloud Stream中提供了消費組的概念
實現輪詢分組,每次只有一個消費者,生產者傳送的訊息只能被一個消費者接收到,避免重複消費

將案例的兩個消費者變成相同組
同一個組的多個微服務例項,每次只會有一個拿到

持久化

配置好 group 這個屬性後可以發現
當消費者發生一些錯誤停止服務時,但是此時的生產者還在不斷的傳送訊息,如果消費者沒有配置 group ,那麼這些訊息就被錯過了
當配置好 group 這個屬性,消費者就算髮生一些錯誤停止服務,再啟動時,就會獲取到之前停止服務期間生產者發來的訊息

相關文章