stream 分组_Stream的消息分组

三、Stream的消息分组

在上个案例中,如果有多个消息接收者,那么消息生产者发送的消息会被多个消费者都接收到,这种情况在某些实际场景下是有很大问题的,比如在如下场景中,订单系统我们做集群部署,都会从RabbitMQ中获取订单信息,那如果一个订单同时被两个服务获取到,那么就会造成数据错误,我们得避免这种情况。这时我们就可以使用Stream中的消息分组来解决了!

还有一个问题:上个案例创建的队列是临时队列(AD,auto-delete),会随着服务的关闭而消失。

3d6fbf6cb5c9d2be8b6022d8a273743d.png

Stream消息分组

消息分组的作用我们已经介绍了。注意在Stream中处于同一个group中的多个消费者是竞争关系。就能够保证消息只会被其中一个应用消费一次。不同的组是可以消费的,同一个组内会发生竞争关系,只有其中一个可以消费。通过案例我们来演示看看,这里我们会创建3个服务,分别如下

0d6309c5d9e5fb1dd74e6fee9ac65a68.png

1分组中的消息发送者

1.1创建项目

d71b51fef9a03c3b9a43983a93876292.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

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

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

7a20124dad325b5904b245c13a465007.png

c176fcddf2bb6037368391a758024130.png

fb18a692a8e47ef404688d4284c87349.png

如果stream-group-receiver启动时的

# 具体分组 对应 MQ 是 队列名称 并且持久化队列 inputProduct 自定义

spring.cloud.stream.bindings.inputProduct.group=groupProduct

是不一样的,那么就会都收到消息

四、Stream的消息分区

相同消息发送到相同的服务中

1创建项目

将上篇中的分组的俩个项目,拷贝一份修改名称及服务名称

97a1a191a41696c5a797e4ffc3a9ba81.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

d94340c81d746fc2131144f12c461596.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

10个消息都被消费者B给消费了,说明到达了我们需要的效果。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值