1.Stream消息驱动是什么

2.Stream消息驱动案例说明

3.Stream消息驱动之生产者

4.Stream消息驱动之消费者

5.Stream消息驱动分组消费和持久化

1.Stream消息驱动是什么

1.1)队列和服务耦合带来的问题
在我们的日常开发当中,会使用到很多的队列,比如有rabbitmq,kafka,rocketMQ,kafka等等。

所以我们在编写项目的时候,项目会和队列的api进行耦合当我们切换队列,或者队列与队列之间传输信息的时候,这种中间件的差异性会给我们造成极大的困扰,如果我们用了一种队列,后面又有新的业务需求,我们想往另外一种消息队列进行迁移,这个时候无疑就是一种灾难性的,很多东西都要推倒重做,因为这些队列和我们的系统耦合了。

1.2)springCloud stream是什么?
springCloud stream是一个让开发者调用上层api,就能屏蔽底层队列的区别,构建消息驱动服务的框架。

所以我们只需要搞清楚如何与Spring Cloud Stream 交互就可以方便使用消息驱动的方式。

目前仅支持RabbitMQ、Kafka。
image.png

简单地来说,spring cloud stream就是一种屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型。

1.3)为什么要使用spring cloud stream

当我们提出为什么要使用一个工具的时候,我们要想不使用这个工具会有什么后果?
我们从上面的描述可以看出,stream消息驱动是一个解耦工具,不使用这个工具,应用程序就会和队列进行耦合,为了解除这种耦合,我们采用了spring cloud stream。

1.4)stream凭什么可以统一底层差异?

我们可以先来看一下,传统的mq是如何工作的?
image.png

传统的mq模型分为生产者,消费者,消息通道。
生产者把消息通过消息通道发送给消费者。

我们再来看一下spring Cloud Stream给我们提供了一种解耦的方式。

image.png

这里引出了一个很重要的概念Binder,在没有Binder这个概念的情况下,我们的springBoot应用要直接与消息中间件进行信息交互,由于消息中间件构建的理念不同,它们的实现细节上会有较大的差异,使用方法也会有很大的差异,通过定义Binder为中间层,完美地实现了应用程序与消息中间件之间的隔离

1.5)Spring Cloud Stream的几个重要概念

image.png

image.png

Binder:
INPUT对应于消费者。
OUTPUT对应于生产者。

这里我们可能会感觉到很奇怪,我们平时不是认为input是生产者,output是消费者吗?
其实我们只要转一个方向就明白了:
out是发送信息的那一方,是生产者。
input是信息的输入方,是消费者。
而我们平时可能是站在消息通道的角度来看的,in是输入,是生产,out是输出,是消费。
我们站消息通道的在外面,思考一下这个问题,就明白了。

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

Source和Sink:
Source:消息发送者
Sink:消息接受者

1.6)常用注解

组成说明B
@Input消息输入通道,消息的消费者
@OutPut消息输出通道,消息的生产者
@StreamListener监听队列,用于消费者的队列的消息接收
@EnableBinding指信道channel和exchange绑定在一起

2.Stream消息驱动案例说明

我们创建三个项目,两个为消费者,一个为生产者。

3.Stream消息驱动之生产者

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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>service-stream-provider-8601</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>service-stream-provider-8601</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR1</spring-cloud.version>
    </properties>
    <dependencies>
        <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>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

yml:

server:
  port: 8601

eureka:
  instance:
    hostname: service-stream-provider #eureka服务端的实例名称
    prefer-ip-address: true #访问路径可以显示IP地址
    instance-id: ${spring.cloud.client.ip-address}:${server.port}-service-stream-provider #访问路径的名称格式
  client:
    register-with-eureka: true     #false表示不向注册中心注册自己。
    fetch-registry: true     #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
    service-url:
      #集群指向其它eureka
      defaultZone: http://127.0.0.1:8001/eureka/,http://127.0.0.1:8002/eureka/
spring:
  rabbitmq: #队列的基本配置
    host: localhost
    port: 5672
    username: guest
    password: guest
  application:
    name: service-stream-provider
  cloud:
    stream:
      bindings:
        output: #生产者 因为out是出,是发送消息的一方,所以是生产者
          destination: studyExchange # 表示要使用的Exchange名称定义
          content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
          binder: rabbit # 设置要绑定的消息服务的具体设置
feign:
  hystrix:
    enabled: true

测试类:

public interface IMessageProvider {
    String send() ;
}
import com.example.demo.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;

/**
 * @author sulingfeng
 * @title: MessageProviderImpl
 * @projectName CloudFamily
 * @description: TODO
 * @date 2022/4/13 17:01
 */
@EnableBinding(Source.class) // 可以理解为是一个消息的发送管道的定义
public class MessageProviderImpl implements IMessageProvider {

    @Resource
    private MessageChannel output; // 消息的发送管道

    @Override
    public String send() {
        String serial = UUID.randomUUID().toString();
        this.output.send(MessageBuilder.withPayload(serial).build()); // 创建并发送消息
        System.out.println("***serial: " + serial);
        return serial;
    }
}
@RestController
public class ProviderController {

    @Resource
    private IMessageProvider messageProvider;

    @GetMapping(value = "/sendMessage")
    public String sendMessage()
    {
        return messageProvider.send();
    }

}

4.Stream消息驱动之消费者

消费者我们创建两个,不过代码大部分都是一样的。

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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>service-stream-consumer-8702</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>service-stream-consumer-8702</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR1</spring-cloud.version>
    </properties>
    <dependencies>
        <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>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

yml:

server:
  port: 8702

eureka:
  instance:
    hostname: service-stream-provider #eureka服务端的实例名称
    prefer-ip-address: true #访问路径可以显示IP地址
    instance-id: ${spring.cloud.client.ip-address}:${server.port}-service-stream-provider #访问路径的名称格式
  client:
    register-with-eureka: true     #false表示不向注册中心注册自己。
    fetch-registry: true     #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
    service-url:
      #集群指向其它eureka
      defaultZone: http://127.0.0.1:8001/eureka/,http://127.0.0.1:8002/eureka/
spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
  application:
    name: service-stream-provider
  cloud:
    stream:
      bindings:
        input: #消费者  消息的输入方 
          destination: studyExchange # 表示要使用的Exchange名称定义
          content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
          binder: rabbit # 设置要绑定的消息服务的具体设置
feign:
  hystrix:
    enabled: true

测试类

@Component
@EnableBinding(Sink.class)//消息的消费者
public class ReceiveMessageListener {
    @Value("${server.port}")
    private String serverPort;

    @StreamListener(Sink.INPUT)//和队列绑定
    public void input(Message message)
    {
        System.out.println("消费者,------->接收到的消息:" + message.getPayload()+"\t port: "+serverPort);
    }
}

5.Stream消息驱动分组消费和持久化

接下来我们启动消费者和生产者,然后生产者发送一条消息,会发现消费者确实收到了消息不过两个消费者都收到了消息,这可能就会造成消息的重复消费

image.png

image.png

这个时候,我们就可以用stream中的消息分组来解决
在stream中,处于同一个group的多个消费者是竞争关系就能够保证一条消息只能被其中一个服务消费
不同组是可以重复消费的
同一组内有竞争关系,只能被其中一个组消费

修改yml:

server:
  port: 8702

eureka:
  instance:
    hostname: service-stream-provider #eureka服务端的实例名称
    prefer-ip-address: true #访问路径可以显示IP地址
    instance-id: ${spring.cloud.client.ip-address}:${server.port}-service-stream-provider #访问路径的名称格式
  client:
    register-with-eureka: true     #false表示不向注册中心注册自己。
    fetch-registry: true     #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
    service-url:
      #集群指向其它eureka
      defaultZone: http://127.0.0.1:8001/eureka/,http://127.0.0.1:8002/eureka/
spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
  application:
    name: service-stream-provider
  cloud:
    stream:
      bindings:
        input: #消费者  消息的输入方
          destination: studyExchange # 表示要使用的Exchange名称定义
          content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
          binder: rabbit # 设置要绑定的消息服务的具体设置
          group: atguiguA
feign:
  hystrix:
    enabled: true

加入分组之后,我们就可以避免重复消费了。
生产者:
image.png

消费者1:

image.png
消费者2:
image.png

解决了重复的消息,此时我们来看看持久化,其实加上group属性就加上了持久化,我们把两个消费者服务都关掉,修改yml,一个有group,一个没有group,然后连续发几条信息,再启动消费者,发现有group的服务的消费者依然可以消费服务离线时候的消息,但是没有group的消费者,就没法消费那些在它这个活动离线的时候消费的消息了。

image.png

image.png

image.png


苏凌峰
73 声望38 粉丝

你的迷惑在于想得太多而书读的太少。