十二、SpringCloud 微服务之SpringCloud Stream 消息驱动

54 篇文章 1 订阅
2 篇文章 0 订阅


一、SpringCloud 微服务基础介绍
二、SpringCloud 微服务项目构建
三、SpringCloud 微服务之Eureka 服务注册与发现
四、SpringCloud 微服务之 Zookeeper 服务注册与发现
五、SpringCloud 微服务之Consul 服务注册与发现
六、SpringCloud 微服务之 Ribbon 负载均衡服务调用
七、SpringCloud 微服务之 OpenFeign 服务接口调用
八、SpringCloud 微服务之 Hystrix 断路器
九、Spring Cloud 微服务之 Gateway
十、SpringCloud 微服务之SpringCloud Config 分布式配置中心
十一、SpringCloud 微服务之SpringCloud Bus 消息总线
十二、SpringCloud 微服务之SpringCloud Stream 消息驱动
十三、Spring Cloud 微服务之 SpringCloud Sleuth 分布式请求链路跟踪
十四、SpringCloud Alibaba Nacos 服务注册和配置中心
十五、SpringCloud Alibaba Sentinel 实现熔断与限流
十六、SpringCloud Alibaba Seata 处理分布式事务

SpringCloud Stream 消息驱动

1. 消息驱动概述

1.1 是什么

屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型

什么是Spring Cloud Stream
官方定义: Spring Cloud Stream 是一个构建消息驱动的框架


应用程序通过inputs或者outputs来与Spring Cloud Stream中的binder(绑定器)对象交互
通过我们配置来binding(绑定),而Spring Cloud Stream 的binder对象负责与消息中间件交互
通过使用Spring Integration来连接消息代理中间件以实现消息事件驱动
Spring Cloud Stream为一些供应商的消息中间件产品提供了个性化的自动化配置实现,引用了发布-订阅、消费组、分区的三个核心概念
目前仅支持RabbitMQ、Kafka

官网文档:
https://spring.io/projects/spring-cloud-stream
https://docs.spring.io/spring-cloud-stream/docs/3.1.3/reference/html/index.html

1.2 设计思想

1. 标准的MQ

在这里插入图片描述

  • 生产者/消费者之间靠消息媒介传递信息内容:Message
  • 消息必须走特定的通道:MessageChannel
  • 消息通道里的消息如何被消费呢?谁负责收发处理
    消息通道MessageChannel的子接口SubscribeChannel,由MessageHandler消息处理器所订阅
2. 为什么用CloudStream
  • Stream凭什么可以统一底层差异
    在没有绑定器这个概念的情况下,我们的SpringBoot应用要直接与消息中间件进行信息交互的时候,由于各消息中间件构建的初衷不通,它们的实现细节上会有较大的差异性,通过定义绑定器作为中间层,完美地实现了应用程序与消息中间件细节之间的隔离
    通过向应用程序暴露统一的Channel通道,使得应用程序不需要再考虑各种不同的消息中间件实现

通过定义绑定器Binder作为中间层,实现了应用与消息中间件细节之间的隔离

  • Binder
    在没有绑定器这个概念的情况下,我们的SpringBoot应用要直接与消息中间件进行信息交互的时候,由于各消息中间件构建的初衷不通,它们的实现细节上会有较大的差异性,通过定义绑定器作为中间层,完美地实现了应用程序与消息中间件细节之间的隔离。Stream对消息中间件的进一步封装,可以做到代码层面对中间件的无感知,甚至于动态的切换中间件(rabbitmq切换为kafka),使得微服务的开发高度解耦,服务可以更多关注自己的业务流程
    在这里插入图片描述
    通过定义绑定器Binder作为中间件,实现了应用程序与消息中间件细节之间的隔离。
3. Stream中的消息通讯方式遵循了发布-订阅模式

Topic 主题进行广播

  • 在RabbitMQ就是Exchange
  • 在Kafka中就是Topic

1.3 Springcloud Stream标准流程套路

在这里插入图片描述
在这里插入图片描述

  • Binder:很方便的连接中间件,屏蔽差异
  • Channel:通道,是队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过channel对队列进行配置
  • Source和Sink:简单的可理解为参照对象是Springcloud Stream自身,从Stream发布消息就是输出,接受消息就是输入

1.4 编码API和常用注解

在这里插入图片描述

2. 案例说明

2.1 RebbitMQ环境已经OK

参考:https://blog.csdn.net/zhou_zhao_xu/article/details/119378590

2.2 新建三个子模块

cloud-stream-rabbitmq-provider8801,作为生产者进行消息模块
cloud-stream-rabbitmq-consumer8802,作为消费者接受模块
cloud-stream-rabbitmq-consumer8803,作为消费者接受模块

3. 消息驱动之生产者

3.1 在父项目中创建 cloud-stream-rabbitmq-provider8801 生产者module

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>springcloud</artifactId>
        <groupId>com.zzx</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-stream-rabbitmq-provider8801</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>

        <!-- 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>

        <dependency>
            <groupId>com.zzx</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>

</project>
2. 编写 yaml 配置文件
server:
  port: 8801
spring:
  application:
    name: cloud-stream-privider # 注册进 Eureka 服务器的微服务名
  cloud:
    stream:
      # 在此配置要绑定的rabbitmq的服务信息
      binders:
        # 表示定义的名称,用于与binding整合
        defaultRabbit:
          # 消息组件类型
          type: rabbit
      # 服务的整合处理
      bindings:
        # 这个名字是一个通道的名称
        output:
          # 表示要使用的Exchange名称定义
          destination: studyExchange
          # 设置消息类型,本次为JSON,文本则设置为 text/plain
          content-type: application/json
          # 设置要绑定的消息服务的具体设置
          binder: defaultRabbit

  # rabbitmq相关配置
  rabbitmq:
    host: 47.107.124.79
    port: 5672
    username: admin
    password: admin

eureka:
  instance:
    instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${server.port} # 设置实例访问地址
    prefer-ip-address: true # 设置访问路径可以显示 ip 地址
  client:
    # 表示将自己注册进 Eureka Server 默认为 true
    register-with-eureka: true
    # 是否从 Eureka Server 抓取自己已有的注册信息,默认为 true。
    # 单节点无所谓,集群必须设置为 true 才能配合 ribbon使用负载均衡
    fetch-registry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka
3. 编写主启动类
@SpringBootApplication
@EnableEurekaClient
public class MainStream8801 {
    public static void main(String[] args){
        SpringApplication.run(MainStream8801.class,args);
    } 
}
4. 编写业务类和controller类

业务类

public interface IMessageProvider {
    String send();
}

package com.zzx.springcloud.service.impl;

import java.util.UUID;

import com.zzx.springcloud.service.IMessageProvider;
import org.springframework.beans.factory.annotation.Autowired;
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;

/**
 * Created with IntelliJ IDEA.
 *
 * @ProjectName: springcloud
 * @ClassName: IMessageProviderImpl
 * @author: wb-zcx696752
 * @description:
 * @data: 2021/8/5 9:43 PM
 */
@EnableBinding(Source.class)
public class IMessageProviderImpl implements IMessageProvider {

    /**
     * 消息发送管道
     */
    private final MessageChannel output;

    @Autowired
    public IMessageProviderImpl(MessageChannel output) {
        this.output = output;
    }

    @Override
    public String send() {
        String serial = UUID.randomUUID().toString();
        boolean send = output.send(MessageBuilder.withPayload(serial).build());
        System.out.println("*********serial : " + serial);
        System.out.println("*********send : " + send);
        return serial + "\t" + send;
    }
}

controller

@RestController
public class SendMessageController {
    private final IMessageProvider iMessageProvider;

    @Autowired
    public SendMessageController(IMessageProvider iMessageProvider) {this.iMessageProvider = iMessageProvider;}

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

启动 Eureka7001、RabbitMQ、启动8801
访问测试
在这里插入图片描述

4. 消息驱动之消费者

4.1 在父项目中创建 cloud-stream-rabbitmq-consumer8802 消费者者module

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>springcloud</artifactId>
        <groupId>com.zzx</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-stream-rabbitmq-consumer8802</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>

        <!-- 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>

        <dependency>
            <groupId>com.zzx</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>

</project>
2. 编写 yaml
server:
  port: 8802
spring:
  application:
    name: cloud-stream-consumer # 注册进 Eureka 服务器的微服务名
  cloud:
    stream:
      # 在此配置要绑定的rabbitmq的服务信息
      binders:
        # 表示定义的名称,用于与binding整合
        defaultRabbit:
          # 消息组件类型
          type: rabbit
      # 服务的整合处理
      bindings:
        # 这个名字是一个通道的名称
        input:
          # 表示要使用的Exchange名称定义
          destination: studyExchange
          # 设置消息类型,本次为JSON,文本则设置为 text/plain
          content-type: application/json
          # 设置要绑定的消息服务的具体设置
          binder: defaultRabbit

  # rabbitmq相关配置
  rabbitmq:
    host: 47.107.124.79
    port: 5672
    username: admin
    password: admin

eureka:
  instance:
    instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${server.port} # 设置实例访问地址
    prefer-ip-address: true # 设置访问路径可以显示 ip 地址
  client:
    # 表示将自己注册进 Eureka Server 默认为 true
    register-with-eureka: true
    # 是否从 Eureka Server 抓取自己已有的注册信息,默认为 true。
    # 单节点无所谓,集群必须设置为 true 才能配合 ribbon使用负载均衡
    fetch-registry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka
3. 编写启动类
@SpringBootApplication
@EnableEurekaClient
public class MainStreamMQ8802 {
    public static void main(String[] args){
        SpringApplication.run(MainStreamMQ8802.class,args);
    } 
}
4. 编写监听类
package com.zzx.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("8802消费者,-------> 接收到的消息:" + message.getPayload() + "\t" + serverPort);
    }
}
5. 测试 启动eureka7001 、生产者8801、消费者8802

在这里插入图片描述
在这里插入图片描述

5. 分组消费与持久化

5.1 在父项目中创建 cloud-stream-rabbitmq-consumer8803 消费者者module

参照 8802 clone一份 port 改为8803

1. 启动测试

启动 RabbitMQ、eureka7001、生产者8801、消费者8802、消费者8803
在这里插入图片描述

2. 运行后存在两个问题
  1. 重复消费问题
  2. 消息持久化问题
3. 消费

测试
目前8802/8803同时都消费到了,存在重复消费的问题
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  1. 解决重复消费的问题
    分组和持久化属性group:重要
  2. 生产的实际案例
    比如在如下场景中,订单系统我们做集群部署,都会从RabbitMQ中获取订单信息,如果一个订单同时被两个服务获取到,那么就会造成数据错误,我们要避免这种情况。这时我们就可以使用Stream中的消息分组来解决
    在这里插入图片描述
    注意:在Stream中处于同一个group中的多个消费者是竞争关系,就能够保证消息只会被其中一个应用消费一次。
    不同组是可以全面消费的(重复消费)
    同一组内会发生竞争关系,只有其中一个可以消费
  3. 重复消费的原因:
    在这里插入图片描述
    故障现象:重复消费
    导致原因:默认分组group是不同的,组流水号不一样,是 不同组,可以重复消费
    解决办法:自定义配置分组,解决重复消费的问题
4. 分组
4.1 原理

微服务应用放置于同一个group中,就能够保证消息只会被其中一个应用消费一次。
不同组是可以重复消费的,同一个组内会发生竞争关系,只有其中一个可以消费

4.2 8802/8803分为不同组,两个group不同

group:groupA、groupB

8802修改YML

server:
  port: 8802
spring:
  application:
    name: cloud-stream-consumer # 注册进 Eureka 服务器的微服务名
  cloud:
    stream:
      # 在此配置要绑定的rabbitmq的服务信息
      binders:
        # 表示定义的名称,用于与binding整合
        defaultRabbit:
          # 消息组件类型
          type: rabbit

      # 服务的整合处理
      bindings:
        # 这个名字是一个通道的名称
        input:
          # 表示要使用的Exchange名称定义
          destination: studyExchange
          # 设置消息类型,本次为JSON,文本则设置为 text/plain
          content-type: application/json
          # 设置要绑定的消息服务的具体设置
          binder: defaultRabbit
          group: groupA

  # rabbitmq相关配置
  rabbitmq:
    host: 47.107.124.79
    port: 5672
    username: admin
    password: admin

eureka:
  instance:
    instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${server.port} # 设置实例访问地址
    prefer-ip-address: true # 设置访问路径可以显示 ip 地址
  client:
    # 表示将自己注册进 Eureka Server 默认为 true
    register-with-eureka: true
    # 是否从 Eureka Server 抓取自己已有的注册信息,默认为 true。
    # 单节点无所谓,集群必须设置为 true 才能配合 ribbon使用负载均衡
    fetch-registry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka

8803修改YML

server:
  port: 8803
spring:
  application:
    name: cloud-stream-consumer # 注册进 Eureka 服务器的微服务名
  cloud:
    stream:
      # 在此配置要绑定的rabbitmq的服务信息
      binders:
        # 表示定义的名称,用于与binding整合
        defaultRabbit:
          # 消息组件类型
          type: rabbit
      # 服务的整合处理
      bindings:
        # 这个名字是一个通道的名称
        input:
          # 表示要使用的Exchange名称定义
          destination: studyExchange
          # 设置消息类型,本次为JSON,文本则设置为 text/plain
          content-type: application/json
          # 设置要绑定的消息服务的具体设置
          binder: defaultRabbit
          group: groupB

  # rabbitmq相关配置
  rabbitmq:
    host: 47.107.124.79
    port: 5672
    username: admin
    password: admin

eureka:
  instance:
    instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${server.port} # 设置实例访问地址
    prefer-ip-address: true # 设置访问路径可以显示 ip 地址
  client:
    # 表示将自己注册进 Eureka Server 默认为 true
    register-with-eureka: true
    # 是否从 Eureka Server 抓取自己已有的注册信息,默认为 true。
    # 单节点无所谓,集群必须设置为 true 才能配合 ribbon使用负载均衡
    fetch-registry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka

我们自己配置group
在这里插入图片描述
发现配置不同的组时,可以重复消费

4.3 8803/8802分为相同组,两个group相同

group:groupA
修改 8802/8803 yaml

server:
  port: 8802/8803
spring:
  application:
    name: cloud-stream-consumer # 注册进 Eureka 服务器的微服务名
  cloud:
    stream:
      # 在此配置要绑定的rabbitmq的服务信息
      binders:
        # 表示定义的名称,用于与binding整合
        defaultRabbit:
          # 消息组件类型
          type: rabbit
      # 服务的整合处理
      bindings:
        # 这个名字是一个通道的名称
        input:
          # 表示要使用的Exchange名称定义
          destination: studyExchange
          # 设置消息类型,本次为JSON,文本则设置为 text/plain
          content-type: application/json
          # 设置要绑定的消息服务的具体设置
          binder: defaultRabbit
          group: groupA

  # rabbitmq相关配置
  rabbitmq:
    host: 47.107.124.79
    port: 5672
    username: admin
    password: admin

eureka:
  instance:
    instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${server.port} # 设置实例访问地址
    prefer-ip-address: true # 设置访问路径可以显示 ip 地址
  client:
    # 表示将自己注册进 Eureka Server 默认为 true
    register-with-eureka: true
    # 是否从 Eureka Server 抓取自己已有的注册信息,默认为 true。
    # 单节点无所谓,集群必须设置为 true 才能配合 ribbon使用负载均衡
    fetch-registry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka

测试结果:同一个组的多个微服务实例,每次只会有一个可以消费到数据

5. 持久化

通过上述方式,解决了重复消费的问题

添加了自定义分组后,group会持久化,当没有该group的消费者时,消息会堆积,当该group有消费者时,会把堆积的数据进行消费

测试:
停止8802/8803;除掉8802的分组group:atguiguA;8003的分组属性保留
8801先发送n条消息到rabbitmq
先启动8802,无分组属性配置,后天没有打出来消息
再启动8803,有分组属性配置,后台打出来了MQ上的消息

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值