本篇文章为系列文章,未读第一集的同学请猛戳这里:Spring Cloud 系列之 Stream 消息驱动(一)

本篇文章讲解 Stream 如何实现消息分组和消息分区。

  

消息分组

  

  点击链接观看:Stream 消息分组视频(获取更多请关注公众号「哈喽沃德先生」)

  

  如果有多个消息消费者,那么消息生产者发送的消息会被多个消费者都接收到,这种情况在某些实际场景下是有很大问题的,比如在如下场景中,订单系统做集群部署,都会从 RabbitMQ 中获取订单信息,如果一个订单消息同时被两个服务消费,系统肯定会出现问题。为了避免这种情况,Stream 提供了消息分组来解决该问题。

  在 Stream 中处于同一个 group 中的多个消费者是竞争关系,能够保证消息只会被其中一个应用消费。不同的组是可以消费的,同一个组会发生竞争关系,只有其中一个可以消费。通过 spring.cloud.stream.bindings.<bindingName>.group 属性指定组名。

  

问题演示

  

  在 stream-demo 项目下创建 stream-consumer02 子项目。

  项目代码使用入门案例中消息消费者的代码。

  单元测试代码如下:

package com.example;

import com.example.producer.MessageProducer;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest(classes = {StreamProducerApplication.class})
public class MessageProducerTest { @Autowired
private MessageProducer messageProducer; @Test
public void testSend() {
messageProducer.send("hello spring cloud stream");
} }

  

测试

  

  运行单元测试发送消息,两个消息消费者控制台打印结果如下:

  stream-consumer 的控制台:

message = hello spring cloud stream

  stream-consumer02 的控制台:

message = hello spring cloud stream

  通过结果可以看到消息被两个消费者同时消费了,原因是因为它们属于不同的分组,默认情况下分组名称是随机生成的,通过 RabbitMQ 也可以得知:

  

配置分组

  

  stream-consumer 的分组配置为:group-A

server:
port: 8002 # 端口 spring:
application:
name: stream-consumer # 应用名称
rabbitmq:
host: 192.168.10.101 # 服务器 IP
port: 5672 # 服务器端口
username: guest # 用户名
password: guest # 密码
virtual-host: / # 虚拟主机地址
cloud:
stream:
bindings:
# 消息接收通道
# 与 org.springframework.cloud.stream.messaging.Sink 中的 @Input("input") 注解的 value 相同
input:
destination: stream.message # 绑定的交换机名称
group: group-A

  

  stream-consumer02 的分组配置为:group-A

server:
port: 8003 # 端口 spring:
application:
name: stream-consumer # 应用名称
rabbitmq:
host: 192.168.10.101 # 服务器 IP
port: 5672 # 服务器端口
username: guest # 用户名
password: guest # 密码
virtual-host: / # 虚拟主机地址
cloud:
stream:
bindings:
# 消息接收通道
# 与 org.springframework.cloud.stream.messaging.Sink 中的 @Input("input") 注解的 value 相同
input:
destination: stream.message # 绑定的交换机名称
group: group-A

  

测试

  

  运行单元测试发送消息,此时多个消息消费者只有其中一个可以消费。RabbitMQ 结果如下:

  

消息分区

  

  点击链接观看:Stream 消息分区视频(获取更多请关注公众号「哈喽沃德先生」)

  

  通过消息分组可以解决消息被重复消费的问题,但在某些场景下分组还不能满足我们的需求。比如,同时有多条同一个用户的数据发送过来,我们需要根据用户统计,但是消息被分散到了不同的集群节点上了,这时我们就可以考虑使用消息分区了。

  当生产者将消息发送给多个消费者时,保证同一消息始终由同一个消费者实例接收和处理。消息分区是对消息分组的一种补充。

问题演示

  

  先给大家演示一下消息未分区的效果,单元测试代码如下:

package com.example;

import com.example.producer.MessageProducer;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest(classes = {StreamProducerApplication.class})
public class MessageProducerTest { @Autowired
private MessageProducer messageProducer; @Test
public void testSend() {
for (int i = 1; i <= 10; i++) {
messageProducer.send("hello spring cloud stream");
}
} }

  

测试

  

  运行单元测试发送消息,两个消息消费者控制台打印结果如下:

  stream-consumer 的控制台:

message = hello spring cloud stream
message = hello spring cloud stream
message = hello spring cloud stream
message = hello spring cloud stream
message = hello spring cloud stream

  stream-consumer02 的控制台:

message = hello spring cloud stream
message = hello spring cloud stream
message = hello spring cloud stream
message = hello spring cloud stream
message = hello spring cloud stream

  假设这 10 条消息都来自同一个用户,正确的方式应该都由一个消费者消费所有消息,否则系统肯定会出现问题。为了避免这种情况,Stream 提供了消息分区来解决该问题。

  

配置分区

  

  消息生产者配置分区键的表达式规则消息分区的数量

server:
port: 8001 # 端口 spring:
application:
name: stream-producer # 应用名称
rabbitmq:
host: 192.168.10.101 # 服务器 IP
port: 5672 # 服务器端口
username: guest # 用户名
password: guest # 密码
virtual-host: / # 虚拟主机地址
cloud:
stream:
bindings:
# 消息发送通道
# 与 org.springframework.cloud.stream.messaging.Source 中的 @Output("output") 注解的 value 相同
output:
destination: stream.message # 绑定的交换机名称
producer:
partition-key-expression: payload # 配置分区键的表达式规则
partition-count: 2 # 配置消息分区的数量

  通过 partition-key-expression 参数指定分区键的表达式规则,用于区分每个消息被发送至对应分区的输出 channel

  该表达式作用于传递给 MessageChannelsend 方法的参数,该参数实现 org.springframework.messaging.Message 接口的 GenericMessage 类。

  

  源码 MessageChannel.java

package org.springframework.messaging;

@FunctionalInterface
public interface MessageChannel {
long INDEFINITE_TIMEOUT = -1L; default boolean send(Message<?> message) {
return this.send(message, -1L);
} boolean send(Message<?> var1, long var2);
}

  源码 GenericMessage.java

package org.springframework.messaging.support;

import java.io.Serializable;
import java.util.Map;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils; public class GenericMessage<T> implements Message<T>, Serializable {
private static final long serialVersionUID = 4268801052358035098L;
private final T payload;
private final MessageHeaders headers; ... }

  

  如果 partition-key-expression 的值是 payload,将会使用所有放在 GenericMessage 中的数据作为分区数据。payload 是消息的实体类型,可以为自定义类型比如 UserRole 等等。

  如果 partition-key-expression 的值是 headers["xxx"],将由 MessageBuilder 类的 setHeader() 方法完成赋值,比如:

package com.example.producer;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component; /**
* 消息生产者
*/
@Component
@EnableBinding(Source.class)
public class MessageProducer { @Autowired
private Source source; /**
* 发送消息
*
* @param message
*/
public void send(String message) {
source.output().send(MessageBuilder.withPayload(message).setHeader("xxx", 0).build());
} }

  

  消息消费者配置消费者总数当前消费者的索引开启分区支持

  stream-consumer 的 application.yml

server:
port: 8002 # 端口 spring:
application:
name: stream-consumer # 应用名称
rabbitmq:
host: 192.168.10.101 # 服务器 IP
port: 5672 # 服务器端口
username: guest # 用户名
password: guest # 密码
virtual-host: / # 虚拟主机地址
cloud:
stream:
instance-count: 2 # 消费者总数
instance-index: 0 # 当前消费者的索引
bindings:
# 消息接收通道
# 与 org.springframework.cloud.stream.messaging.Sink 中的 @Input("input") 注解的 value 相同
input:
destination: stream.message # 绑定的交换机名称
group: group-A
consumer:
partitioned: true # 开启分区支持

  stream-consumer02 的 application.yml

server:
port: 8003 # 端口 spring:
application:
name: stream-consumer # 应用名称
rabbitmq:
host: 192.168.10.101 # 服务器 IP
port: 5672 # 服务器端口
username: guest # 用户名
password: guest # 密码
virtual-host: / # 虚拟主机地址
cloud:
stream:
instance-count: 2 # 消费者总数
instance-index: 1 # 当前消费者的索引
bindings:
# 消息接收通道
# 与 org.springframework.cloud.stream.messaging.Sink 中的 @Input("input") 注解的 value 相同
input:
destination: stream.message # 绑定的交换机名称
group: group-A
consumer:
partitioned: true # 开启分区支持

  

测试

  

  运行单元测试发送消息,此时多个消息消费者只有其中一个可以消费所有消息。RabbitMQ 结果如下:

  至此 Stream 消息驱动所有的知识点就讲解结束了。

本文采用 知识共享「署名-非商业性使用-禁止演绎 4.0 国际」许可协议

大家可以通过 分类 查看更多关于 Spring Cloud 的文章。

  

Spring Cloud 系列之 Stream 消息驱动(二)的更多相关文章

  1. Spring Cloud 系列之 Stream 消息驱动(一)

    在实际开发过程中,服务与服务之间通信经常会使用到消息中间件,消息中间件解决了应用解耦.异步处理.流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构. 不同中间件内部实现方式是不一样的,这些中间 ...

  2. Spring Cloud 系列之 Bus 消息总线

    什么是消息总线 消息代理中间件构建一个共用的消息主题让所有微服务实例订阅,当该消息主题产生消息时会被所有微服务实例监听和消费. 消息代理又是什么?消息代理是一个消息验证.传输.路由的架构模式,主要用来 ...

  3. Spring Cloud 系列之 Consul 配置中心

    前面我们已经学习过 Spring Cloud Config 了: Spring Cloud 系列之 Config 配置中心(一) Spring Cloud 系列之 Config 配置中心(二) Spr ...

  4. spring cloud 2.x版本 Spring Cloud Stream消息驱动组件基础教程(kafaka篇)

    本文采用Spring cloud本文为2.1.8RELEASE,version=Greenwich.SR3 本文基于前两篇文章eureka-server.eureka-client.eureka-ri ...

  5. Spring Cloud系列(二) 介绍

    Spring Cloud系列(一) 介绍 Spring Cloud是基于Spring Boot实现的微服务架构开发工具.它为微服务架构中涉及的配置管理.服务治理.断路器.智能路由.微代理.控制总线.全 ...

  6. Spring Cloud 系列之 Spring Cloud Stream

    Spring Cloud Stream 是消息中间件组件,它集成了 kafka 和 rabbitmq .本篇文章以 Rabbit MQ 为消息中间件系统为基础,介绍 Spring Cloud Stre ...

  7. Spring Cloud 系列之 Sleuth 链路追踪(二)

    本篇文章为系列文章,未读第一集的同学请猛戳这里:Spring Cloud 系列之 Sleuth 链路追踪(一) 本篇文章讲解 Sleuth 基于 Zipkin 存储链路追踪数据至 MySQL,Elas ...

  8. Spring Cloud 系列之 Consul 注册中心(二)

    本篇文章为系列文章,未读第一集的同学请猛戳这里:Spring Cloud 系列之 Consul 注册中心(一) 本篇文章讲解 Consul 集群环境的搭建. Consul 集群 上图是一个简单的 Co ...

  9. Spring Cloud 系列之 Gateway 服务网关(二)

    本篇文章为系列文章,未读第一集的同学请猛戳这里:Spring Cloud 系列之 Gateway 服务网关(一) 本篇文章讲解 Gateway 网关的多种路由规则.动态路由规则(配合服务发现的路由规则 ...

随机推荐

  1. 001_Three.js中的跨域问题

    001_Three.js中的跨域问题 [情景描述]: 在初始化模型,引入字体和纹理皮肤图片的时候,由于跨域问题,出现了以下提示: Access to image at 'file:///F:/User ...

  2. 《Three.js 入门指南》3.1.1 - 基本几何形状 - 平面(PlaneGeometry)

    3.1 基本几何形状 平面(PlaneGeometry) 说明: 这里的平面(PlaneGeometry)其实是一个长方形,而不是数学意义上无限大小的平面. 构造函数: THREE.PlaneGeom ...

  3. 从测试点点君跨入年薪30W的自动化逍遥君的人生感悟--测试君请进,绝对让你不虚此行!

    一.前言:人生感悟 人生,就是一个苏醒的过程,生命就是一次历练,从鲜衣怒马,到银碗里盛雪,从青葱岁月到白发染鬓,人总是会在经历中成长,在成长中懂得,在懂得里看透,看透而不说透,从而一步一步的走向成熟, ...

  4. Altium 分形天线设计

    Altium 分形天线设计 程序运行界面 Cantor三分集 Koch雪花 Sierpinski垫片 源代码: Iter_Num = 4     'diedai PI = 3.1415926 Call ...

  5. MTK Android Driver :Camera

    MTK Android Driver :camera 1.相关代码位置:mediatek\config\XXXX(红色字为具体的项目名) 文件:ProjectConfig.mk CUSTOM_KERN ...

  6. Array(数组)对象-->concat() 方法

    1.定义和用法 concat() 方法用于连接两个或多个字符串. 语法: string.concat(string1, string2, ..., stringX) 举例: var str1='hel ...

  7. HAproxy 基础配置

    基础配置详解 HAProxy 的配置文件haproxy.cfg由两大部分组成,分别是global和proxies部分 global:全局配置段 进程及安全配置相关的参数性能调整相关参数Debug参数 ...

  8. 学习《深入应用c++11》1

    C++11 让程序更简洁.更优雅 可调用对象 是一个函数指针 是一个具有operator()成员函数的类对象(仿函数) 是一个可被装换为函数指针的类对象 是一个类的成员(函数)指针 void func ...

  9. git获取特定的commit

    git reset --hard [commit_id]

  10. AJ学IOS(28)UI之Quartz2D简单介绍

    AJ分享,必须精品 iOS开发UI篇—Quartz2D简单介绍 什么是Quartz2D Quartz 2D是⼀个二维绘图引擎,同时支持iOS和Mac系统 Quartz 2D能完成的工作: 绘制图形 : ...