SpringCloud-Stream

什么是SpringCloudStream

概述

Spring Cloud Stream是一个框架,用于构建与共享消息系统连接的高度可扩展的事件驱动微服务。就是一种消息驱动框架。目前只支持RabbitMQKafka

主要解决哪些问题?

对于一个平台,可能使用了多种消息中间件,比如同时使用了RabbitMQKafka等消息中间键,对于程序员,可能不能学到所有的消息中间件。于是Spring Cloud Stream产生了,它的作用就是屏蔽底层MQ的差异,将MQ的操作封装成一个模型,我们只需要对这个模型进行操作,不用管底层的多种MQ之间如何工作,类似与JDBC技术,解决了多种数据库之间的差异性。

中文参考手册:https://blog.csdn.net/qq_32734365/article/details/81413218

工作方式

应用程序只需要通过 inputsoutputs来与SpringCloud Stream 中的Binder经行交互,也就是说我们只需要配置Bingding(绑定),而Spring Cloud Stream的 Binder对象负责与消息中间件经行交互,所以我们只需要知道如何与Spring Cloud Stream交互,就能操作各种不同的消息中间件了。Spring Integration为消息代理提供链接。

原理图:

在微服务与微服务之间,我们只需要绑定好Binder,我们需要接收消息就使用inputs 需要发送消息就使用outputs,具体在消息中间件中如何交互,我们是无需关心的。

开发实际运行原理图:

Binder:很方便的连接中间件,屏蔽差异

Channel:通道,是队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过对Channel对队列进行配置

Source和Sink:简单的可理解为参照对象是Spring Cloud Stream自身,从Stream发布消息就是输出,接受消息就是输入

编码API和常用注解

SpringCloudStream使用

  1. 父项目搭建,参考:https://www.cnblogs.com/Rampant/p/14770855.html
  2. 准备RabbitMQ环境,参考:https://dg.

消息生产者搭建

  1. 创建项目:cloud-stream-rabittmq-provider-5000

  2. 导入pom.xml

        <dependencies>
            <!--
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-stream-binder-rabbit</artifactId>
            </dependency>
            二者任选其一
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
            </dependency>
            -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
            </dependency>
    
            <!--连接 consul 的依赖-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-consul-discovery</artifactId>
            </dependency>
    
            <!--spring-boot-web 模块 常用的3个-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter</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>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-test</artifactId>
                <scope>test</scope>
            </dependency>
            <!--lombok 依赖-->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
            </dependency>
        </dependencies>
    
  3. application.yaml

    server:
      port: 5000
    
    spring:
      application:
        name: cloud-stream-provider
      rabbitmq:
        host: 47.97.218.81
        port: 5672
        username: admin
        password: 970699
      cloud:
        stream:
          bindings: # 服务的整合处理
            output: # 这个名字是一个通道的名称
              destination: stream.message # 表示要使用的Exchange名称定义
    
      consul:
        # 服务的主机
        host: localhost
        # 服务的端口号
        port: 8500
        discovery:
          # 注册到服务中心的名称
          service-name: ${spring.application.name}
    
  4. 主启动类

    package com.wyx.cloud;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
    
    @SpringBootApplication
    @EnableDiscoveryClient
    public class StreamRabbitMQMain5000 {
        public static void main(String[] args) {
            SpringApplication.run(StreamRabbitMQMain5000.class,args);
        }
    }
    
  5. 业务类

    package com.wyx.cloud.service;
    
    public interface IMessageProvider {
        public String send();
    }
    
    package com.wyx.cloud.service;
    
    import org.springframework.cloud.stream.annotation.EnableBinding;
    import org.springframework.cloud.stream.messaging.Source;
    import org.springframework.context.annotation.Bean;
    import org.springframework.integration.support.MessageBuilder;
    import org.springframework.stereotype.Component;
    
    import javax.annotation.Resource;
    import java.util.UUID;
    import java.util.function.Function;
    
    @Component
    @EnableBinding(Source.class)
    public class IMessageProviderImpl implements IMessageProvider{
    
        @Resource
        Source source;
    
        @Override
        public String send() {
            String serial = UUID.randomUUID().toString();
            source.output().send(MessageBuilder.withPayload(serial).build());
            return null;
        }
    }
    
    package com.wyx.cloud.controller;
    
    import com.wyx.cloud.service.IMessageProvider;
    import lombok.extern.log4j.Log4j2;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import javax.annotation.Resource;
    
    @RestController
    @Log4j2
    public class SendMessageController {
    
        @Resource
        private IMessageProvider messageProvider;
    
        @GetMapping("/send")
        public String sendMessage(){
            log.info("开始发送消息");
            messageProvider.send();
            return null;
        }
    }
    

消息消费者搭建

  1. 创建模块 cloud-stream-rabbitmq-consumer-5001

  2. pom.xml

    <dependencies>
        <!--
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-stream-binder-rabbit</artifactId>
            </dependency>
            二者任选其一
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
            </dependency>
            -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
        </dependency>
    
        <!--连接 consul 的依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>
    
        <!--spring-boot-web 模块 常用的3个-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</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>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-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--lombok 依赖-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
    
  3. application.yaml

    server:
      port: 5001
    
    spring:
      application:
        name: cloud-stream-consumer
      rabbitmq:
        host: 47.97.218.81
        port: 5672
        username: admin
        password: 970699
    
      cloud:
        stream:
          bindings: # 服务的整合处理
            input: # 这个名字是一个通道的名称
              destination: stream.message # 表示要使用的Exchange名称定义
    
      consul:
        # 服务的主机
        host: localhost
        # 服务的端口号
        port: 8500
        discovery:
          # 注册到服务中心的名称
          service-name: ${spring.application.name}
    
  4. 主启动类

    package com.wyx.cloud;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
    
    @SpringBootApplication
    @EnableDiscoveryClient
    public class StreamMQConsumerMain5001 {
    
        public static void main(String[] args) {
            SpringApplication.run(StreamMQConsumerMain5001.class,args);
        }
    }
    
  5. 业务类

    package com.wyx.cloud.service;
    
    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 GetMessage {
        
        @StreamListener(Sink.INPUT)
        public void getMessage(Message message){
            System.out.println(message);
        }
    }
    

启动consul 在启动两个服务,访问 http://localhost:5000/send 发送消息,查看5001端口控制台即可

自定义信道

参考Source.java,和 Sink.java创建自定义通道

  1. MySource.java

    package com.wyx.cloud.channer;
    
    import org.springframework.cloud.stream.annotation.Output;
    import org.springframework.messaging.MessageChannel;
    
    public interface MySource {
    
        // 信道的名字,需要配合配置文件中的通道名字使用
        String MY_OUTPUT = "my_output";
    
        @Output(MY_OUTPUT)
        MessageChannel myOutput();
    
    }
    
  2. MySink.java

    package com.wyx.cloud.channer;
    
    import org.springframework.cloud.stream.annotation.Input;
    import org.springframework.messaging.SubscribableChannel;
    
    public interface MySink {
    	// 信道的名字,需要配合配置文件中的通道名字使用
        String MY_INPUT = "my_input";
    
        @Input(MY_INPUT)
        SubscribableChannel myInput();
    }
    
  3. 修改application.yaml

    spring:
      cloud:
        stream:
          bindings: # 服务的整合处理
            my_input: # 这里表示信道名称,和我们自定义的信道名字绑定
              destination: my.message # 表示要使用的Exchange名称定义
    
    spring:
      cloud:
        stream:
          bindings: # 服务的整合处理
            my_output: # 这个名字是一个通道的名称
              destination: my.message # 表示要使用的Exchange名称定义
    

启动测试,效果依然可以。

配置优化

​ 默认情况下,如果没有配置

spring:
  cloud:
    stream:
      bindings: # 服务的整合处理
        my_input: # 这里表示信道名称,和我们自定义的信道名字绑定
          destination: my.message # 表示要使用的Exchange名称定义

则他默认使用信道的名称作为交换机名称。即我们可以如下优化

  1. 删除application.yaml中的如下配置

    spring:
      cloud:
        stream:
          bindings: # 服务的整合处理
            my_input: # 这里表示信道名称,和我们自定义的信道名字绑定
              destination: my.message # 表示要使用的Exchange名称定义
    
    spring:
      cloud:
        stream:
          bindings: # 服务的整合处理
            my_output: # 这个名字是一个通道的名称
              destination: my.message # 表示要使用的Exchange名称定义
    
  2. 修改自定义的信道的信道名称,如下

    package com.wyx.cloud.channer;
    
    import org.springframework.cloud.stream.annotation.Output;
    import org.springframework.messaging.MessageChannel;
    
    public interface MySource {
    
        String DEFAULT = "default.exchange";
    
        @Output(DEFAULT)
        MessageChannel myOutput();
    
    }
    
    package com.wyx.cloud.channer;
    
    import org.springframework.cloud.stream.annotation.Input;
    import org.springframework.messaging.SubscribableChannel;
    
    public interface MySink {
    
        String DEFAULT = "default.exchange";
    
        @Input(DEFAULT)
        SubscribableChannel myInput();
    }
    
  3. 修改监听端口的service

    package com.wyx.cloud.service;
    
    import com.wyx.cloud.channer.MySink;
    import org.springframework.cloud.stream.annotation.EnableBinding;
    import org.springframework.cloud.stream.annotation.StreamListener;
    import org.springframework.messaging.Message;
    import org.springframework.stereotype.Component;
    
    @Component
    @EnableBinding(MySink.class)
    public class GetMessage {
    
        @StreamListener(MySink.DEFAULT)//x
        public void getMessage(Message message){
            System.out.println(message);
        }
    
    }
    
    

消息分组

什么是消息分组,对于一个高可用的系统,微服务集群是必须的存在,但是当我们使用上面的方式将消费者微服务集群后,那么当我们在向交换机写信息时,同时两个微服务简单一个交换机,那么消息会在两个微服务上都消费,这样会破坏系统的容错性。于是为了解决这个问题,我们可以对消息进行分组。

还有一个特点:加了分组之后,如果消息消费服务突然宕机,那么这时候如果消息生产者继续生产消息,当消息消费者恢复后能继续消费未消费的消息,也就是消息具有持久化功能

环境准备:在搭建一个消息消费者cloud-stream-rabbitmq-consumer-5002具体参考上面消息消费者搭建,修改端口即可。将5001还原成5002的样子,5000还原最初状态

修改5000的业务类的IMessageProviderImpl.java

package com.wyx.cloud.service;



import com.wyx.cloud.channer.MySource;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.stereotype.Component;

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

@Component
//@EnableBinding(MySource.class)
@EnableBinding(Source.class)
public class IMessageProviderImpl implements IMessageProvider{

//    @Resource
//    MySource source;
    @Resource
    Source source;

    @Override
    public String send() {
        String serial = UUID.randomUUID().toString();
//        source.myOutput().send(MessageBuilder.withPayload(serial).build());
        for (int i = 0; i < 10; i++) {
            source.output().send(MessageBuilder.withPayload(serial).build());
        }
        return null;
    }
}

分区步骤:

  1. 修改 5001和5002的配置文件application.yaml

    spring:
      cloud:
        stream:
          bindings: # 服务的整合处理
            input: # 这个名字是一个通道的名称
              destination: stream.message # 表示要使用的Exchange名称定义
              group: group #添加一个分组,配置即可实现消息不重复消费。
    

消息分区

在上面的消息分组中,他解决的消息重复消费的问题,但是对于有一些特殊消息,他存在一定级联关系,发送者发送的消息(一次性发送进队列),只能由一个微服务来处理,那么我们可以使用消息分区来进行处理。

消息分区配置:

消息生产者:

spring:
  cloud:
    stream:
      bindings: # 服务的整合处理
        output: # 这个名字是一个通道的名称
          destination: stream.message # 表示要使用的Exchange名称定义
          producer:		# 添加消息分区
            partition-key-expression: payload     # 配置分区规则,还有一种设置head头的,利用MessageBuilder.withPayload(serial).setHeader()来配置
            partition-count: 2    # 分区数量,即几个微服务

消息消费者

spring:
  cloud:
    stream:
      instance-count: 2   # 消费者总数
      instance-index: 0   #当前消费者索引,重0开始,根据不同的端口切换
      bindings: # 服务的整合处理
        input: # 这个名字是一个通道的名称
          destination: stream.message # 表示要使用的Exchange名称定义
          group: group
          consumer:		# 开启分区支持
            partitioned: true
posted @ 2021-07-23 20:36  橘子有点甜  阅读(167)  评论(0编辑  收藏  举报