17. Spring Cloud Stream消息驱动持久化

Spring Cloud Stream消息驱动

为什么引入消息驱动?

首先看到消息驱动,我们会想到消息中间件,比如以下几种

  • ActiveMQ
  • RabbitMQ
  • RocketMQ
  • Kafka

在这里插入图片描述

但是在实际开发中,可能会遇到一些问题,比如说上图的中台和后台可能存在两种MQ,它们之间的实现都是不一样的,这样会导致多种问题出现,而且我们也看到了,目前主流的MQ有四种,我们不可能每个都去学习

那有没有一技术,能让我们不再关注具体MQ的细节,只需要用一种适配绑定的方式,就可以自动的在各种MQ内切换呢?

这个时候,Spring Cloud Stream诞生了,解决的痛点就是屏蔽了消息中间件底层的细节差异,我们只需要操作Stream就可以操作各种消息中间件了,大大降低了开发成本。

简介

什么是消息驱动

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

有点像Hibernate,它同时支持多种数据库,同时还提供了Hibernate Session的语法,也就是HQL语句,这样屏蔽了SQL具体实现细节,我们只需要操作HQL语句,就能够操作不同的数据库

什么是Spring Cloud Stream

Spring Cloud Stream是一个构建消息驱动微服务的框架

应用程序通过inputs或者outputs来与Spring Cloud Stream中binder对象(绑定器)交互。

binder对象屏蔽了底层的差异性,统一了编程风格,我们主要就是通过它去操作不同的消息中间件。

我们只需要搞清楚如何与Spring Cloud Stream中的binder对象(绑定器)交互,就可以方便的使用消息驱动。

通过使用Spring Integration来连接消息代理中间件以实现消息事件驱动。

Spring Cloud Stream为一些供应商的消息中间件产品提供了个性化的自动化配置实现,引用了发布-订阅,消费组,分区的三个核心概念

Spring Cloud Stream目前仅支持RabbitMQ 和 Kafka

Spring Cloud Stream设计思想

在这里插入图片描述

标准MQ

生产者/消费者之间靠消息媒介传递消息内容

  • Message

消息必须走特定的通道

  • MessageChannel

消息通道里的消息如何被消费呢,谁负责收发处理

  • 消息通道MessageChannel的子接口SubscribableChannel,由MessageHandler消息处理器所订阅
为什么用SpringCloudStream

比如说开发中用到了RabbitMQ和Kafka,由于这两个消息中间件的架构上不同

像RabbitMQ有exchange,kafka有Tpic和Partitions分区

在这里插入图片描述

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

SpringCloudStream为什么能屏蔽底层差异

通过向应用程序暴露统一的Channel通道,使得应用程序不需要再考虑各种不同消息中间件的实现。

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

Binder
  • input:对应消费者
  • output:对应生产者

Stream对消息中间件的进一步封装,可以做到代码层面对中间件的无感知,甚至于动态的切换中间件(RabbitMQ切换Kafka),使得微服务开发的高度解耦,服务可以关注更多的自己的业务流程。

在这里插入图片描述

Stream中的消息通信方式遵循了发布-订阅模式,Topic主题进行广播,在RabbitMQ中就是Exchange,在Kafka中就是Topic

Stream标准流程套路

我们的消息生产者和消费者只和Stream交互

在这里插入图片描述

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

在这里插入图片描述

案例说明

前提是已经安装好了RabbitMQ

  • cloud-stream-rabbitmq-provider8801,消息生产者模块
  • cloud-stream-rabbitmq-consumer8802,消息接收模块
  • cloud-stream-rabbitmq-consumer8803,消息接收模块

消息驱动之生产者

实现

New Module: cloud-stream-rabbitmq-provider8801
pom
		<!--stream中的rabbit模块-->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
		</dependency>

		<!--添加Eureka Client依赖-->

		<!--自定义的api工具包,可以使用Payment支付Entity-->

		<!--boot web actuator-->

		<!--通用配置-->
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: # 客户端进行Eureka注册的配置
    service-url:
      defaultZone: http://localhost:7001/eureka
  instance:
    lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
    lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
    instance-id: send-8801.com  # 在信息列表时显示主机名称
    prefer-ip-address: true     # 访问的路径变为IP地址
main
package com.indi.springcloud;

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

MessageProvider.java

package com.indi.springcloud.service;

public interface MessageProvider
    /**
     * 发送消息的接口
     */
    String send();
}
ServiceImpl

MessageProviderImpl.java

package com.indi.springcloud.service.impl;

@EnableBinding(Source.class)    // 定义消息的推送管道,导stream下的包,别导错了!!
public class MessageProviderImpl implements MessageProvider {
    @Resource
    private MessageChannel output;  // 消息发送通道

    @Override
    public String send() {
        String serial = IdUtil.randomUUID();
        output.send(MessageBuilder.withPayload(serial).build());    // 将准备好的消息发送到RabbitMQ,导integration下的包,别导错了!!
        System.out.println("*****serial:" + serial);
        return null;
    }
}
Controller

SendMessageController.java

@RestController
public class SendMessageController{
	@Resource
	private MessageProvider messageProvider;
	
	@GetMapping("/sendMessage")
	public String sendMessage(){
		return messageProvider.send();
	}
}

测试

RabbitMQ后台:http://localhost:15672/

在这里插入图片描述

测试链接:http://localhost:8801/sendMessage

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

消息驱动之消费者

实现

New Module: cloud-stream-rabbitmq-consumer8802
pom

同8801

yml

在这里插入图片描述

main
package com.indi.springcloud;

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

ReceiveMessageListenerController.java

@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("消费者1号:----->" + message.getPayload()+"\t port:"+ serverPort)
	}
}

测试

启动7001、8801、8802

还是发送几次请求,测试链接:http://localhost:8801/sendMessage

在这里插入图片描述

结果8801、8802都打出了相应的消息

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

再进入RabbitMQ后台看看:http://localhost:15672/

在这里插入图片描述

在这里插入图片描述

重复消费

再创建一个8803的消费者模块

测试

启动7001、8801、8802、8803

我们再次尝试让8801发送消息,结果8802 、8803同时都收到了,存在重复消费的问题

什么是重复消费

比如在如下场景中,订单系统我们做集群部署,都会从RabbitMQ中获取订单信息,那如果一个订单同时被两个服务获取到,那么就会造成数据错误,我们需要避免这种情况,可以使用Stream中的消息分组来解决。

在这里插入图片描述

在Stream中只要消费者存在竞争关系,那就能够保证每次消息只被一个消费者消费

  • 只有消费者在同一组才存在竞争关系
  • 两个不同组的消费者是不存在竞争关系的,也就是说会导致重复消费

多数情况下,生产者发送消息给某个具体微服务时,只希望被消费一次,按照上面我们启动两个应用的例子,虽然它们同属一个应用,但它们两个在不同的组,所以才出现了消息被重复消费两次的情况,为了解决这个情况,我们需要将它们两个放到一个组内,这样它们就会产生竞争关系,消息就不会被重复消费。

分组

在这里插入图片描述

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

当然,如果是这样的话,还是会产生重复消费的问题,这里只是演示一下如何分组。

在这里插入图片描述

然后我们将两个消费者放到同一个组内,比如都放到indiA

在这里插入图片描述

我们在8801,发送了6条消息

在这里插入图片描述

8802,收到了3条

在这里插入图片描述

8802,也收到了3条

在这里插入图片描述

最后因为8802、8803都在同一个组内,然后它们通过轮询的机制,都得到了消息,也没有造成重复消费。

持久化

通过上面的方式,我们已经解决了重复消费的问题,再来看看持久化。

测试

  • 停止8802和8803,并移除8802的group,保留8803的group
  • 8801先发送4条消息到RabbitMQ
  • 启动8802,因为没有分组属性,所以后台没有打出来消息
  • 再启动8803,因为有分组属性,后台打出了8801发送的消息
  • 还有一种情况就是,两个服务都不移除group信息,哪个服务先启动,哪个服务就会先抢到这期间的消息。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值