Spring Cloud Stream消息驅動

一、消息驅動概述

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 測試

  1. 啓動7001eureka
  2. 啓動rabbitmq
  3. rabbitmq-plugins enable rabbitmq_management

http://localhost:15672/

  1. 啓動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:15672/#/

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 持久化

  1. 停止8802/8803並去除掉8802的分組group:atguiguA,8803的分組group:atguiguA沒有去掉
  2. 8801先發送4條消息到rabbitmq
  3. 先啓動8802,去掉了分支atguiguA,無分組屬性配置,後臺沒有打出來消息
  4. 再啓動8803,有分組屬性配置,後臺打出來了MQ上的消息

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章