Stream前置
- 为了解决一个完整系统中有多个消息中间件(MQ),它们的切换、维护、开发会很麻烦。希望只需要用一种适配绑定的方式,自动给我们在各种MQ中切换。Stream就是可以屏蔽底层的这些插件,能够操作各种MQ,降低开发难度。
- 屏蔽底层消息中间件的差异,降低切换成本,同一消息的编程模型。
- Spring Cloud Stream 是一个构建消息驱动微服务的框架。应用程序通过inputs或者outputs,来与Spring Cloud Stream中的binder对象交互。通过我们配置来binding(绑定),而Spring Cloud Stream的binder对象负责与消息中间件交互。所以,我们只需要搞清楚如何与Spring Cloud Stream交互就可以方便使用消息驱动的方式。
- 通过使用Spring Integration来连接消息代理中间件,以实现消息事件驱动。Spring Cloud Stream为一些供应商的消息中间件提供了个性化的自动配置实现,引用了发布-订阅、消费组、分区的三个核心概念。
- 目前只支持RabbitMQ,Kafka;
版本说明
- cloud Hoxton.SR1
- boot 2.2.2RELEASE
- cloud alibaba 2.1.0 RELEASE
- java java8
- Maven 3.5以上
- Mysql 5.7以上
Stream
消息驱动概述
-
标准MQ
-
生产者/消费者之间靠消息媒介传递信息内容:Message;
-
消息必须走特定的通道,消息通道MessageChannel;
-
消息通道里的消费如何被消费呢,谁负责收发处理?答:消息通道MessageChannel的子接口SubscribableChannel,由MessageHandler消息处理器所订阅。
-
-
Stream设计思想
-
通过定义绑定器,作为中间层,完美实现应用程序与消息中间件细节之间的隔离;通过向应用程序暴露统一的Channel通道,使得应用程序不需要再考虑各种不同的消息中间件实现。
- 通过定义绑定器Binder作为中间层,实现了应用程序与消息中间件之间的隔离。
- Binder,INPUT对应于消费者,OUTPUT对应于生产者。
-
Stream中的消息通信方式遵循了发布-订阅模式。
- 通过Topic主题进行广播,在RabbitMQ就是Exchange,在Kafka中就是Topic;
-
- Spring Cloud Stream标准流程
* Binder:很方便的连接中间件,屏蔽差异;
* Channel:通道,是队列Queue的一种抽象,在消息通讯系统中,就是实现存储和转发的媒介,通过Channel对队列进行配置。
* Source和Sink:简单的可以理解为,参照对象是Spring Cloud Stream自身,从Stream发布消息就是输出,接受消息就是输入。
- 编码API和常用注解:
案例说明
- RabbitMQ环境搭建完成;
- 工程中新建三个子模块:
- 子模块:
- cloud-stream-rabbitmq-provider8801,作为生产者,进行发消息模块;
- cloud-stream-rabbitmq-consumer8802,消息接收模块
- cloud-stream-rabbitmq-consumer8803,消息接收模块
- 子模块:
消息驱动之生产者
-
cloud-stream-rabbitmq-provider8801,作为生产者,进行发消息模块;
-
创建、POM、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 #设置心跳的时间间隔(默认是30S) lease-expiration-duration-in-seconds: 5 #如果超过5S间隔就注销节点 默认是90s instance-id: send-8801.com #在信息列表时显示主机名称 prefer-ip-address: true #访问的路径变为IP地址
-
-
业务类:
-
发送消息接口;
-
发送消息接口实现类;
-
//这里是跟rabbitMQ打交道的,不是被Controller所调用,因此不加@Service
-
-
service
//这里是跟rabbitMQ打交道的,不是被Controller所调用,因此不加@Service @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 null; } }
-
Controller;
@RestController public class SendMessageController { @Resource private IMessageProvider messageProvider; @GetMapping(value = "/sendMessage") public String sendMessage(){ return messageProvider.send(); } }
-
测试:启动7001、7002、启动rabbitMQ、启动8801、访问http://localhost:8801/sendMessage
-
-
消息驱动之消费者
-
新建Module,cloud-stream-rabbitmq-consumer8802;
-
POM,YML,主启动类,业务类,测试8801发送8802接收消息;
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://eureka7001.com:7001/eureka,http://eureka7002.com:7002/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地址
-
controller业务类
@Component @EnableBinding(Sink.class)//将信道channel和exchange绑定在一起 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); } }
-
测试:8801发送,8802接收消息;http://localhost:8801/sendMessage
-
分组消费的问题
-
依照8802,clone出一份8803,cloud-stream-rabbitmq-consumer8803
-
启动:
- RabbitMQ;
- eureka7001,7002;
- 8801,消费生产;
- 8802,消费消费
- 8803,消费消费
-
运行后的问题:
-
有重复消费的问题;
- 比如在如下场景中,订单系统我们做集群部署,都会从RabbitMQ中获取订单信息。那么如果一个订单同时被两个服务获取到,就会造成数据错误,我们得避免这种情况。这时我们就可以使用Stream中得消息分组来解决。注意:在Stream中处于同一个group中得多个消费者是竞争关系,就能保证消息只会被其中一个应用消费一次。但不同组是可以全面消费(重复消费)的。
-
说明这是两个组别,因为组流水号不一致;
-
微服务应用要放置在同一个group中,就能够保证消息只会被其中一个应用消费一次。不同组是可以消费的,同一个组内会发送竞争关系,只有一个可以消费。
-
自定义分组,先分成两个组:
-
group: atguiguA, atguiguB
-
修改8802和8803的YML,加一个group就行了:
-
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 #设置要绑定的消息服务的具体设置 group: atguiguB
-
-
-
自定义分组,分成一个组:
- 为了让8802和8803实现轮询分组,每次只有一个消费者,8801模块发的消息只能被8802或8803其中一个接收到,这样避免了重复消费。
- 将8802和8803都变成相同的组,group两个组就相同了。group全改为atguiguA即可。
消息持久化的问题
- 停止8802/8803,并去除8802的分组group:atguiguA
- 8801先发送4条消息到rabbitmq;
- 先启动8802,无分组属性配置,后台没有打出消息;
- 再启动8803,有分组属性配置,后台打出了MQ上的消息
需熟练掌握
知识点
tips
- 目前常用的消息中间件(MQ),有:ActiveMQ、RabbitMQ、RocketMQ、Kafka;
- RabbitMQ是一个开源的消息代理和队列服务器,用来通过普通协议在完全不同的应用之间共享数据,或者简单的将作业排队以便让分布式服务器进行处理。
- 启动rabbitMQ
- 进入RabbitMQ安装目录下的sbin目录;
- 输入以下命令,启动管理功能;
- .\rabbitmq-plugins enable rabbitmq_management
- 可视化插件;
- 访问地址查看是否安装成功:http://localhost:15672
- 输入账号密码并登录,默认是guest guest
代码地址:https://github.com/AJ-Spade/cloud2020/tree/master