SpringCloud13-SpringCloud Stream消息驱动

SpringCloud Stream消息驱动

1、概述

1.1 Spring Cloud Stream是什么

官方定义Spring Cloud Stream是一个构建消息驱动微服务的框架。

应用程序通过inputs或者outputs来与Spring Cloud Stream中binder对象交互。通过我们配置来binding(绑定),而Spring Cloud Stream的binder对象负责与消息中间件交互。所以,我们只需要搞清楚如何与Spring Cloud Stream交互就可以方便使用消息驱动的方式。

通过使用Spring Integration来连接消息代理中间件以实现消息事件驱动。Spring Cloud Stream为一些供应商的消息中间件产品提供了个性化的自动化配置实现,引用了发布订阅、消费组、分区的三个核心概念。

目前仅支持RabbitMQ、Kafka.

SpringCloud13-SpringCloud Stream消息驱动_第1张图片

总结一句话:SpringCloud Stream是为了屏蔽底层信息中间件的差异,降低切换成本,统一消息的编程模型

官方地址:

https://spring.io/projects/spring-cloud-stream#overview

https://cloud.spring.io/spring-cloud-static/spring-cloud-stream/3.0.1.RELEASE/reference/html/

https://m.wang1314.com/doc/webapp/topic/20971999.html(中文手册)

1.2 Spring Cloud Stream的设计思想

1.2.1 标准的MQ

  • 生产者/消费者之间靠消息媒介传递消息内容——Message
  • 消息必须走特定的通道——消息通道MessageChannel
  • 消息通道里的消息如何被消费呢,谁负责收发处理——消息通道MessageChannel的子接口SubscribableChannel,由MessageHandler消息处理器所订阅

SpringCloud13-SpringCloud Stream消息驱动_第2张图片

1.2.2 为什么要用Spring Cloud Stream

比方说我们用到了RabbitMQ和Kafka,由于这两个消息中间件的架构上的不同,比如RabbitMQ有exichange,而kafka有 Topic和Partitions分区;

SpringCloud13-SpringCloud Stream消息驱动_第3张图片

这些中间件的差异性导致我们实际项目开发给我们造成了一定的困扰, 我们如果用了两个消息队列的其中一种,后面的业务需求,我想往另外一种消息队列进行迁移,这时候无疑就是一个灾难性的; 一大堆东西都要重新推倒重新做,因为它跟我们的系统耦合了,这时候springcloud Stream给我们提供了一种解耦合的方式。

在没有绑定器这个概念的情况下,我们的SpringBoot应用要直接与消息中间件进行信息交互的时候,由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性。

通过定义绑定器作为中间层,完美地实现了应用程序与消息中间件细节之间的隔离。
通过向应用程序暴露统一的Channel通道,使得应用程序不需要再考虑各种不同的消息中间件实现。

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

SpringCloud13-SpringCloud Stream消息驱动_第4张图片

input对应消费者,output对应生产者

Stream中的消息通信方式遵循了发布—订阅模式,topic主题进行广播(在ribbitmq就是Exchange,在kafka中就是topic)

1.3 Spring Cloud Stream标准流程套路

SpringCloud13-SpringCloud Stream消息驱动_第5张图片

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

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

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

1.4 编码API和常用注解

SpringCloud13-SpringCloud Stream消息驱动_第6张图片

2、消息驱动之生产者

2.1 新建模块cloud-stream-rabbitmq-provider8801

SpringCloud13-SpringCloud Stream消息驱动_第7张图片

2.1.1 pom.xml


<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>springcloud2020artifactId>
        <groupId>com.zdw.springcloudgroupId>
        <version>1.0-SNAPSHOTversion>
    parent>
    <modelVersion>4.0.0modelVersion>

    <artifactId>cloud-stream-rabbitmq-provider8801artifactId>
    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-actuatorartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-stream-rabbitartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-devtoolsartifactId>
            <scope>runtimescope>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
    dependencies>

project>

2.1.2 application.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”
          binder: defaultRabbit #设置要绑定的消息服务的具体设置

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: send-8801.com #在信息列表时显示主机名称
    prefer-ip-address: true #访问的路径变为IP地址

2.1.3 主启动类

package com.zdw.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

/**
 * @author ZDW
 * @create 2020-08-22 10:52
 */
@SpringBootApplication
@EnableEurekaClient
public class ProviderMQMain8801 {
    public static void main(String[] args) {
        SpringApplication.run(ProviderMQMain8801.class,args);
    }
}

2.2 业务类编写

2.2.1 发送消息接口

package com.zdw.springcloud.service;

/**
 * @author ZDW
 * @create 2020-08-22 10:54
 */
public interface IMessageProvider {
    public String send();
}

2.2.2 发送消息实现类

package com.zdw.springcloud.service.impl;

import com.zdw.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;

/**
 * @author ZDW
 * @create 2020-08-22 10:55
 */
@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 serial+"————消息发送成功";
    }
}

2.2.3 controller触发发送消息操作

package com.zdw.springcloud.controller;

import com.zdw.springcloud.service.IMessageProvider;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * @Author ZDW
 * @Since 2020/8/22 11:21
 */
@RestController
public class SendMessageController {

    @Resource
    private IMessageProvider messageProvider;

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

3、消息驱动之消费者

3.1 新建模块cloud-stream-rabbitmq-consumer8802

新建消费者之一:cloud-stream-rabbitmq-consumer8802

SpringCloud13-SpringCloud Stream消息驱动_第8张图片

3.1.1 pom.xml


<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>springcloud2020artifactId>
        <groupId>com.zdw.springcloudgroupId>
        <version>1.0-SNAPSHOTversion>
    parent>
    <modelVersion>4.0.0modelVersion>

    <artifactId>cloud-stream-rabbitmq-consumer8802artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-actuatorartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-stream-rabbitartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-devtoolsartifactId>
            <scope>runtimescope>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
    dependencies>

project>

3.1.2 application.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”
          binder: defaultRabbit #设置要绑定的消息服务的具体设置

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: receive-8802.com #在信息列表时显示主机名称
    prefer-ip-address: true #访问的路径变为IP地址

注意:input: #这个名字是一个通道的名称,在生产者的时候配置的是output(输出)

3.1.3 主启动类

package com.zdw.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

/**
 * @author ZDW
 * @create 2020-08-22 11:06
 */
@SpringBootApplication
@EnableEurekaClient
public class ConsumerMQMain8802 {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerMQMain8802.class,args);
    }
}

3.1.4 接收消息的controller

package com.zdw.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.web.bind.annotation.RestController;

/**
 * @author ZDW
 * @create 2020-08-22 11:08
 */
@RestController
@EnableBinding(Sink.class)
public class ReceiverMessageListenerController {

    @Value("${server.port}")
    private  String serverPort;

    @StreamListener(Sink.INPUT) //监听
    public void input(Message<String> message) {
        System.out.println("消费者8802, -----> 接受到的消息: " + message.getPayload()
                + "\t port: " + serverPort);
    }
}

3.2 新建模块cloud-stream-rabbitmq-consumer8803

参照上面创建8802的步骤,创建8803,这样就搭建了一个生产者,多个消费者的情形。

生产者发布的消息,可以被多个消费者订阅,如果消费者属于不同的组,那么都可以消费该消息,如果消费者属于相同的组,那么就只有组内的某一个消费者可以消费该消息;

4、测试和解决重复消费以及消息持久化问题

4.1 测试消息的发送和接收是否正常

启动eureka7001,然后启动生产者8801,在启动消费者8802和8803

浏览器多次访问:http://localhost:8801/sendMessage

查看8802和8803的控制台,都会接收到8801发送的消息

如果一切正常,说明我们的环境是OK的

4.2 消息重复消费和持久化

在上面的测试中,可以看到生产者发布了一条消息,消费8802和8803都消费了这一条消息,相当于这一条消息被消费了两次;

这个情况在MQ的发布订阅当中也属正常现象,如果业务是这样的话,是没有任何问题的。这里之所以称之为重复消费,是因为如果8802和8803是一个系统的多个副本,那么就不应该被消费两次,只能消费一次。

比如:订单系统生产出一个订单(相当于是一个消息),而8802和8803是库存系统的两个副本,那么正常情况就应该是要么8802把库存减一,要么8803把库存减一,不能两个都进行库存的减少操作。

SpringCloud13-SpringCloud Stream消息驱动_第9张图片

所以要实现上面的需求,我们需要多加一个配置,那就是手动的为消费者进行分组,分组之后,组内的消费者属于竞争关系,同一条消息只能被组内的某一个消费者进行消费。

而且,分组还能达到消息的持久化效果,如果该消费者没有进行手动的分组(在配置文件中配置),那么一旦它宕机,而此时生产者却发送了消息,当消费者重启服务之后,不会接收到在宕机期间,生产者发送的消息,这就造成了消息的丢失。

4.3 修改消费8802和8803的application.yml

修改消费8802和8803的application.yml,给消费者指定分组,让他们两处理同一个分组中:

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

加上了 group: consumer8802 ,使8802和8803都属于组consumer8802

4.4 再次测试

不会接收到在宕机期间,生产者发送的消息,这就造成了消息的丢失。**

4.3 修改消费8802和8803的application.yml

修改消费8802和8803的application.yml,给消费者指定分组,让他们两处理同一个分组中:

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

加上了 group: consumer8802 ,使8802和8803都属于组consumer8802

4.4 再次测试

重启消费者服务,然后重新测试,会发现8801生成的消息,会被8802和8803轮询的消费掉。

你可能感兴趣的:(stream)