Spring Cloud筆記-Spring Cloud Stream消息驅動(十五)

1.消息驅動概述

1.Spring Cloud Stream是什麼

Spring Cloud Stream是一個構建消息驅動微服務的框架。應用程序通過Inpust和Outputs與Spring Cloud中的Binder對象進行交互。首先我們需要配置綁定關係,也就是將具體的消息中間件和Stream進行綁定,後面使用Binder來操作消息中間件。此時,我們只需要瞭解Stream中的Binder對象相關的操作,即可完成消息中間件的交互。

Spring Cloud Stream爲一些供應商的消息中間件產品提供了個性化配置實現,引用了發佈-訂閱、消費組、分區的三個核心概念。目前支持的消息中間件有:RabbitMQ和Kafka。目的在於屏蔽不同消息中間件的差異,統一消息的編程模型。

2.設計思想

對於普通的MQ,生產者和消費者之間依靠消息媒介傳輸信息,消息通過特定的通道進行傳輸,此時Spring Boot和消息中間件直接進行交互,因爲不同消息中間件的設計不同,在實現細節上會有差異。通過定義Binder作爲中間層,實現應用程序和消息中間件實現細節的隔離。Stream對消息中間件的進一步封裝,可以做到代碼層面對中間件的無感知,降低了學習成本。

Inputs對應消息生產者,向Binder發送消息,Binder將消息發給消息中間件,Outputs對應消息消費者,從Binder中取消息,Binder去消息中間件中獲取。

Stream中的消息通信遵循發佈-訂閱模式,也就是通過Topic進行傳播。

3.Spring Cloud Stream標準流程套路

4.編碼API和常用註解

2.案例說明

爲了演示Stream,我們需要再創建3個子模塊,分別是cloud-stream-rabbitmq-provider8801,cloud-stream-rabbitmq-consumer8802,cloud-stream-rabbitmq-consumer8803,另外,需要把RabbitMQ安裝並啓動。

3.消息驅動之生產者

創建cloud-stream-rabbitmq-provider8801模塊,修改pom.xml,加入spring-cloud-starter-stream-rabbit的座標。

<?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.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</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-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>

添加application.yml配置文件。

server:
  port: 8801
spring:
  application:
    name: cloud-stream-provider
  cloud:
    stream:
      binders: # 此處配置要綁定的rabbitmq的服務信息
        binderName: # 表示爲binder對象定義一個名稱,用於binding整合
          type: rabbit # 消息組件類型
          environment: # 設置rabbitmq的相關環境配置
            spring:
              rabbitmq:
                host: 192.168.0.123
                port: 5672
                username: guest
                password: guest
      bindings: # 服務的整合處理
        output: # 一個通道的名稱,不能修改
          destination: exchangeName # 表示RabbitMQ中要使用的Exchange名稱,給Exchange起一個名字
          content-type: application/json # 設置消息類型,本次爲json,文本要設置爲“text/plain”
          binder: binderName # 表示binding對應哪個binder
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka
  instance:
    lease-renewal-interval-in-seconds: 2 # 設置心跳的時間間隔(默認是30S)
    lease-expiration-duration-in-seconds: 5 # 如果超過5S間隔就註銷節點(默認是90s)
    instance-id: provider-8801.com # 在信息列表時顯示主機名稱
    prefer-ip-address: true # 訪問的路徑變爲IP地址

添加主啓動類。

package com.atguigu.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class StreamMQMain8801 {
    public static void main(String[] args) {
        SpringApplication.run(StreamMQMain8801.class, args);
    }
}

添加業務類,這是一個provider模塊,所以需要提供send()方法用於發送消息,添加IMessageProvider和MessageProviderImpl。

package com.atguigu.springcloud.service;

public interface IMessageProvider {
    String send();
}
package com.atguigu.springcloud.service.impl;

import com.atguigu.springcloud.service.IMessageProvider;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.MessageChannel;

import javax.annotation.Resource;
import java.util.UUID;

// 這裏使用@EnableBinding(Source.class),這是Stream裏的註解,定義一個消息推送管道,不需要使用@Service註解
@EnableBinding(Source.class)
public class MessageProviderImpl implements IMessageProvider {
    @Resource
    private MessageChannel output;// 定義消息發送管道,名稱必須是output,否則會報錯

    @Override
    public String send() {
        String uuid = UUID.randomUUID().toString();
        output.send(MessageBuilder.withPayload(uuid).build());
        System.out.println("uuid=" + uuid);
        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 iMessageProvider;

    @GetMapping("/sendMessage")
    public String sendMessage() {
        return iMessageProvider.send();
    }
}

啓動Eureka7001,啓動RabbitMQ服務,啓動Provider8801模塊測試。查看RabbitMQ管理後臺,在Exchange標籤下,可以看到我們定義的exchangeName。瀏覽器訪問http://localhost:8801/sendMessage發送消息,不斷刷新頁面發送請求,在控制檯可以看到消息輸出,在RabbitMQ管理後臺的Overview可以看到消息發送的速率等信息。

4.消息驅動之消費者

新建cloud-stream-rabbitmq-consumer8802模塊,修改pom.xml,依賴和cloud-stream-rabbitmq-provider8801一樣。添加application.yml配置文件,注意這裏通道名稱改成input,之前cloud-stream-rabbitmq-provider8801使用的是output。

server:
  port: 8802
spring:
  application:
    name: cloud-stream-consumer
  cloud:
    stream:
      binders: # 此處配置要綁定的rabbitmq的服務信息
        binderName: # 表示爲binder對象定義一個名稱,用於binding整合
          type: rabbit # 消息組件類型
          environment: # 設置rabbitmq的相關環境配置
            spring:
              rabbitmq:
                host: 192.168.0.123
                port: 5672
                username: guest
                password: guest
      bindings: # 服務的整合處理
        input: # 一個通道的名稱,不能修改
          destination: exchangeName # 表示RabbitMQ中要使用的Exchange名稱,給Exchange起一個名字
          content-type: application/json # 設置消息類型,本次爲json,文本要設置爲“text/plain”
          binder: binderName # 表示binding對應哪個binder
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka
  instance:
    lease-renewal-interval-in-seconds: 2 # 設置心跳的時間間隔(默認是30S)
    lease-expiration-duration-in-seconds: 5 # 如果超過5S間隔就註銷節點(默認是90s)
    instance-id: provider-8802.com # 在信息列表時顯示主機名稱
    prefer-ip-address: true # 訪問的路徑變爲IP地址

添加主啓動類。

package com.atguigu.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class StreamMQMain8802 {
    public static void main(String[] args) {
        SpringApplication.run(StreamMQMain8802.class, args);
    }
}

添加業務類。

package com.atguigu.springcloud.service;

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;

// 這裏使用@EnableBinding(Sink.class),這是Stream裏的註解,定義一個消息接收管道
@EnableBinding(Sink.class)
public class ReceiveMessageService {
    @Value("${server.port}")
    private String serverPort;

    @StreamListener(Sink.INPUT)
    public void input(Message<String> message) {
        System.out.println("ServerPort=" + serverPort + "消費者,消費消息:" + message.getPayload());
    }
}

啓動cloud-stream-rabbitmq-consumer8802模塊,瀏覽器請求http://localhost:8801/sendMessage發送消息,在8802的控制檯,可以看到消息接收的情況,測試通過。

5.分組消費與持久化

參照cloud-stream-rabbitmq-consumer8802模塊,創建cloud-stream-rabbitmq-consumer8803模塊並啓動。瀏覽器請求http://localhost:8801/sendMessage,結果8802和8803都消費了消息,存在重複消費的問題。

在Stream中,處於同一個group的多個消費者是競爭關係,一條消息只能消費一次,處於不同group的消費者是可以重複消費的。通過查看RabbitMQ的管理後臺,點擊exchange標籤,找到我們指定的exchange,發現Bindings裏面有兩個組,所以出現了重複消費問題。所以,解決重複消費的辦法也很簡單,將不需要重複消費的消息指定分組即可。

修改兩個consumer的application.yml,在inputs結點下,加一個group屬性,值爲myGroup。再次發送消息,查看8802和8803的控制檯打印信息,可知同一個組的消費者每次只有一個可以消費消息,而且默認採用輪詢的方式。

添加了group屬性的消費者,在重啓後,會自動獲取未消費的消息,而沒有添加group屬性的消費者,在重啓後,即使有未消費的消息,也無法消費了。比如,停掉8802和8803消費者,去掉8802的group屬性,保留8803的group屬性,生產者發送幾條消息,重啓8802模塊,它並不能處理未消費的消息,重啓8803模塊,因爲它指定了group,它可以拿到未消費的消息,繼續消費,實現了消息的持久化。指定過group的消費者重啓時候,在RabbitMQ的Exchange中,會繼續保留group的名稱,用於下次重啓消費者時候,將消息再次發送給消費者,未指定group名稱的消費者,每次啓動,都會隨機生成一個group放在Exchange的Bindings裏面,當消費者重啓,會把這個隨機值從Bindings裏移除,再加入一個新的隨機值,所以說,未指定group的消費者,無法獲取到之前未消費的消息,也就沒法繼續消費了。

 

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