三、Stream的消息分组
在上个案例中,如果有多个消息接收者,那么消息生产者发送的消息会被多个消费者都接收到,这种情况在某些实际场景下是有很大问题的,比如在如下场景中,订单系统我们做集群部署,都会从RabbitMQ中获取订单信息,那如果一个订单同时被两个服务获取到,那么就会造成数据错误,我们得避免这种情况。这时我们就可以使用Stream中的消息分组来解决了!
还有一个问题:上个案例创建的队列是临时队列(AD,auto-delete),会随着服务的关闭而消失。
![3d6fbf6cb5c9d2be8b6022d8a273743d.png](https://i-blog.csdnimg.cn/blog_migrate/067288d4e142a005204b59e9dbb2fd65.jpeg)
Stream消息分组
消息分组的作用我们已经介绍了。注意在Stream中处于同一个group中的多个消费者是竞争关系。就能够保证消息只会被其中一个应用消费一次。不同的组是可以消费的,同一个组内会发生竞争关系,只有其中一个可以消费。通过案例我们来演示看看,这里我们会创建3个服务,分别如下
![0d6309c5d9e5fb1dd74e6fee9ac65a68.png](https://i-blog.csdnimg.cn/blog_migrate/8eb8267946dc927b0b9e4e2912dc9f85.png)
1分组中的消息发送者
1.1创建项目
![d71b51fef9a03c3b9a43983a93876292.png](https://i-blog.csdnimg.cn/blog_migrate/82dcb0a41a9e614118a6a62255c86e90.png)
1.2修改pom文件
<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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.13.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.bjsxt</groupId>
<artifactId>stream-group-sender</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</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>Dalston.SR5</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>
1.3修改配置文件
配置中的“outputProduct”可以自定义,但是我们等会在消息接口中要使用到。
spring.application.name=stream-group-sender
server.port=9050
#设置服务注册中心地址,指向另一个注册中心
eureka.client.serviceUrl.defaultZone=http://user:123456@eureka1:8761/eureka/,http://user:123456@eureka2:8761/eureka/
#rebbitmq链接信息
spring.rabbitmq.host=192.168.63.141
spring.rabbitmq.port=5672
spring.rabbitmq.username=oldlu
spring.rabbitmq.password=123456
spring.rabbitmq.virtualHost=/
# 对应 MQ 是 exchange outputProduct自定义的信息
spring.cloud.stream.bindings.outputProduct.destination=exchangeProduct
1.4创建ISendeService
package com.bjsxt;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.messaging.SubscribableChannel;
public interface ISendeService {
//注意:这个outputProduct与配置文件中的对应
String OUTPUT = "outputProduct";
//指定输出的交换器名称
@Output(OUTPUT)
SubscribableChannel send();
}
1.5添加实体类
在本案例中我们发送的消息是自定义的对象
package com.bjsxt;
import java.io.Serializable;
public class Product implements Serializable{
private Integer id;
private String name;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Product(Integer id, String name) {
super();
this.id = id;
this.name = name;
}
public Product() {
super();
}
@Override
public String toString() {
return "Product [id=" + id + ", name=" + name + "]";
}
}
1.6启动类
package com.bjsxt;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.stream.annotation.EnableBinding;
@EnableEurekaClient
//绑定我们刚刚创建的发送消息的接口类型
@EnableBinding(value = {ISendeService.class})
@SpringBootApplication
public class SenderApplication {
public static void main(String[] args) {
SpringApplication.run(SenderApplication.class, args);
}
}
1.7修改测试类
package com.bjsxt.test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.test.context.junit4.SpringRunner;
import com.bjsxt.SenderApplication;
import com.bjsxt.ISendeService;
import com.bjsxt.Product;
@RunWith(SpringRunner.class)
@SpringBootTest(classes=SenderApplication.class)
public class StreamTest {
@Autowired
private ISendeService iSendeService;
@Test
public void testSendStream() {
Product product = new Product();
product.setId(100);
product.setName("Hello Stream.....");
// 将需要发送的消息封装为Message对象
Message<Product> message = MessageBuilder.
withPayload(product).
build();
/**
* 第一个send是为了获得订阅管道,第二个才是真正的发送信息
*/
iSendeService.send().send(message);
}
}
2分组中的消息接收者
2.1创建项目
![bfa80003030580022c6f3fd957e52767.png](https://i-blog.csdnimg.cn/blog_migrate/f052249a9133530e1133f6a1720174e8.png)
2.2修改pom文件
<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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.13.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.bjsxt</groupId>
<artifactId>stream-group-sender</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</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>Dalston.SR5</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>
2.3修改配置文件
配置文件中配置分组“groupProduct”
spring.application.name=stream-group-receiver
server.port=9051
#设置服务注册中心地址,指向另一个注册中心
eureka.client.serviceUrl.defaultZone=http://user:123456@eureka1:8761/eureka/,http://user:123456@eureka2:8761/eureka/
#rebbitmq链接信息
spring.rabbitmq.host=192.168.63.141
spring.rabbitmq.port=5672
spring.rabbitmq.username=oldlu
spring.rabbitmq.password=123456
spring.rabbitmq.virtualHost=/
# 对应 MQ 是 exchange 和消息发送者的 交换器是同一个
spring.cloud.stream.bindings.inputProduct.destination=exchangeProduct
# 具体分组 对应 MQ 是 队列名称 并且持久化队列 inputProduct 自定义
spring.cloud.stream.bindings.inputProduct.group=groupProduct
2.4添加实体类
package com.bjsxt;
import java.io.Serializable;
public class Product implements Serializable{
private Integer id;
private String name;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Product(Integer id, String name) {
super();
this.id = id;
this.name = name;
}
public Product() {
super();
}
@Override
public String toString() {
return "Product [id=" + id + ", name=" + name + "]";
}
}
2.5添加IReceiveService
package com.bjsxt;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.messaging.SubscribableChannel;
public interface IReceiveService {
//和配置文件里面的inputProduct相对应
String INPUT = "inputProduct";
//指定输入的交换器名称
@Input(INPUT)
SubscribableChannel receive();
}
2.6创建处理消息的类
package com.bjsxt;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.stereotype.Service;
//处理消息的类
@Service
@EnableBinding({IReceiveService.class})
public class ReceiverService {
@StreamListener(IReceiveService.INPUT)
public void onReceive(Product product) {
//处理消息
System.out.println("ReceiverA: "+product);
}
}
2.7修改启动类
package com.bjsxt;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.stream.annotation.EnableBinding;
@EnableEurekaClient
@EnableBinding(value = {IReceiveService.class})
@SpringBootApplication
public class ReceiveApplication {
public static void main(String[] args) {
SpringApplication.run(ReceiveApplication.class, args);
}
}
3测试分组
3.1消息队列是否是持久化队列
![08da1c861a60b60b3ff64d4c62289a75.png](https://i-blog.csdnimg.cn/blog_migrate/8625da6906f66f8f8e050761cb13dd95.jpeg)
3.2向集群中发送消息测试
- 启动stream-group-receiver
- 修改stream-group-receiver的application.properties,将端口改为9052,以及处理消息的类,修改如下:
package com.bjsxt;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.stereotype.Service;
//处理消息的类
@Service
@EnableBinding({IReceiveService.class})
public class ReceiverService {
@StreamListener(IReceiveService.INPUT)
public void onReceive(Product product) {
//处理消息
System.out.println("ReceiverB: "+product);
}
}
- 再次启动这个stream-group-receiver
- 运行stream-group-sender中的测试类StreamTest
- 查看控制台
![9175c4c08fd27a771f4e17fb14184a73.png](https://i-blog.csdnimg.cn/blog_migrate/39b035d6d5cb153ce67b07b97327745e.png)
![7a20124dad325b5904b245c13a465007.png](https://i-blog.csdnimg.cn/blog_migrate/57b13f73aee47d1b62f13046e233e346.png)
![c176fcddf2bb6037368391a758024130.png](https://i-blog.csdnimg.cn/blog_migrate/65715f08bb916c18af09b4dd3aab3631.png)
![fb18a692a8e47ef404688d4284c87349.png](https://i-blog.csdnimg.cn/blog_migrate/1139c6d242f7f757cb9f7a49cc40db97.png)
如果stream-group-receiver启动时的
# 具体分组 对应 MQ 是 队列名称 并且持久化队列 inputProduct 自定义
spring.cloud.stream.bindings.inputProduct.group=groupProduct
是不一样的,那么就会都收到消息
四、Stream的消息分区
相同消息发送到相同的服务中
1创建项目
将上篇中的分组的俩个项目,拷贝一份修改名称及服务名称
![97a1a191a41696c5a797e4ffc3a9ba81.png](https://i-blog.csdnimg.cn/blog_migrate/9e5ef4a48ed54646cb0187e6fb9c6e2a.png)
2没有分区的情况下演示
发送多条消息查看效果
修改stream-partition-sender的StreamTest:
package com.bjsxt.test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.test.context.junit4.SpringRunner;
import com.bjsxt.SenderApplication;
import com.bjsxt.ISendeService;
import com.bjsxt.Product;
@RunWith(SpringRunner.class)
@SpringBootTest(classes=SenderApplication.class)
public class StreamTest {
@Autowired
private ISendeService iSendeService;
@Test
public void testSendStream() {
Product product = new Product();
product.setId(100);
product.setName("Hello Stream.....");
// 将需要发送的消息封装为Message对象
Message<Product> message = MessageBuilder.
withPayload(product).
build();
/**
* 第一个send是为了获得订阅管道,第二个才是真正的发送信息
*/
for(int i=0;i<10;i++) {
iSendeService.send().send(message);
}
}
}
10条消息被随机的分散到了两个消费者中:
![75c20fd7be0dca4834fd6ee8324966a3.png](https://i-blog.csdnimg.cn/blog_migrate/b4a52760dc7155c0218a7b28eb42ea14.png)
![d94340c81d746fc2131144f12c461596.png](https://i-blog.csdnimg.cn/blog_migrate/40dc0967dc50d462765127aaaaea06e4.png)
我们可以看到A中4条消息,B中6条消息,而且这是随机的,下次执行的结果可能又不一样。
3分区
3.1发送者中配置
spring.application.name=stream-partition-sender
server.port=9050
#设置服务注册中心地址,指向另一个注册中心
eureka.client.serviceUrl.defaultZone=http://user:123456@eureka1:8761/eureka/,http://user:123456@eureka2:8761/eureka/
#rebbitmq链接信息
spring.rabbitmq.host=192.168.63.141
spring.rabbitmq.port=5672
spring.rabbitmq.username=oldlu
spring.rabbitmq.password=123456
spring.rabbitmq.virtualHost=/
# 对应 MQ 是 exchange outputProduct自定义的信息
spring.cloud.stream.bindings.outputProduct.destination=exchangeProduct
#通过该参数指定了分区键的表达式规则
spring.cloud.stream.bindings.outputProduct.producer.partitionKeyExpression=payload
#指定了消息分区的数量。 就是当前你有一个集群,在这个集群中,你有多少个节点要消费这个消息,不能多写不能少写
spring.cloud.stream.bindings.outputProduct.producer.partitionCount=2
3.1消费者中配置
服务A
spring.application.name=stream-partition-receiver
server.port=9051
#设置服务注册中心地址,指向另一个注册中心
eureka.client.serviceUrl.defaultZone=http://user:123456@eureka1:8761/eureka/,http://user:123456@eureka2:8761/eureka/
#rebbitmq链接信息
spring.rabbitmq.host=192.168.63.141
spring.rabbitmq.port=5672
spring.rabbitmq.username=oldlu
spring.rabbitmq.password=123456
spring.rabbitmq.virtualHost=/
# 对应 MQ 是 exchange 和消息发送者的 交换器是同一个
spring.cloud.stream.bindings.inputProduct.destination=exchangeProduct
# 具体分组 对应 MQ 是 队列名称 并且持久化队列 inputProduct 自定义
spring.cloud.stream.bindings.inputProduct.group=groupProduct
#开启消费者分区功能
spring.cloud.stream.bindings.inputProduct.consumer.partitioned=true
#指定了当前消费者的总实例数量
spring.cloud.stream.instanceCount=2
#设置当前实例的索引号,从 0 开始
spring.cloud.stream.instanceIndex=1
服务B
spring.application.name=stream-partition-receiver
server.port=9052
#设置服务注册中心地址,指向另一个注册中心
eureka.client.serviceUrl.defaultZone=http://user:123456@eureka1:8761/eureka/,http://user:123456@eureka2:8761/eureka/
#rebbitmq链接信息
spring.rabbitmq.host=192.168.63.141
spring.rabbitmq.port=5672
spring.rabbitmq.username=oldlu
spring.rabbitmq.password=123456
spring.rabbitmq.virtualHost=/
# 对应 MQ 是 exchange 和消息发送者的 交换器是同一个
spring.cloud.stream.bindings.inputProduct.destination=exchangeProduct
# 具体分组 对应 MQ 是 队列名称 并且持久化队列 inputProduct 自定义
spring.cloud.stream.bindings.inputProduct.group=groupProduct
#开启消费者分区功能
spring.cloud.stream.bindings.inputProduct.consumer.partitioned=true
#指定了当前消费者的总实例数量
spring.cloud.stream.instanceCount=2
#设置当前实例的索引号,从 0 开始
spring.cloud.stream.instanceIndex=0
3.3测试
- 以消费者A启动stream-partition-receiver,以消费者B启动stream-partition-receiver
- 运行stream-partition-sender的StreamTest类中的方法
![53b5e6817643c01c461b9b0fafbe8e8b.png](https://i-blog.csdnimg.cn/blog_migrate/a5afd2e65b33b9d95da19454abc6fe0c.png)
10个消息都被消费者B给消费了,说明到达了我们需要的效果。