玩轉(zhuǎn)SpringCloud Stream

背景及痛點(diǎn)

現(xiàn)如今消息中間件(MQ)在互聯(lián)網(wǎng)項(xiàng)目中被廣泛的應(yīng)用,特別是大數(shù)據(jù)行業(yè)應(yīng)用的特別的多,現(xiàn)在市面上也流行這多個(gè)消息中間件框架,比如ActiveMQRabbitMQRocketMQKafka等,這些消息中間件各有各的優(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 Streambinder對(duì)象負(fù)責(zé)與消息中間件交互,所以我們只需要搞清楚如何與SpringCloud Stream交互即可方便的使用消息中間件。

SpringCloud Stream通過Spring Integration來連接消息代理中間件以實(shí)現(xiàn)消息事件驅(qū)動(dòng),它提供了個(gè)性化的自動(dòng)化配置,引用了發(fā)布訂閱消費(fèi)組分區(qū)的三個(gè)核心概念,但是目前僅支持RabbitMQKafka

設(shè)計(jì)思想

在此之前

以前的架構(gòu)

生產(chǎn)者和消費(fèi)者通過消息媒介(queue等)傳遞信息內(nèi)容(Message),消息必須通過特定的通道(MessageChannel),通過消息的發(fā)布與訂閱來決定消息的發(fā)送和消費(fèi)(publish/subscrib)。

引入中間件

現(xiàn)在假如我們用到了RabbitMQKafka,由于這兩個(gè)消息中間件的架構(gòu)上的不同,像RabbitMQExchange,而KafkatopichePartitions分區(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ù)流程。

處理架構(gòu)

其遵循了發(fā)布-訂閱模式,主要使用的就是Topic主題進(jìn)行廣播,RabbitMQ就是Exchange,在Kafka中就是Topic

通過定義綁定器Binder作為中間層,實(shí)現(xiàn)了應(yīng)用程序與消息中間件細(xì)節(jié)之間的隔離。

stream流程

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和注解

常用api和注解

使用示例

基本環(huán)境

  • 注冊中心:Eureka,可以是其他。

  • 消息中間件:RabbitMQ

    rabbitmq:
      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ā)送了多條消息。

生產(chǎn)服務(wù)生產(chǎn)消息

消費(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)這些消息。

消費(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ù)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。