https://blog.csdn.net/songhaifengshuaige/article/details/79264851
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/songhaifengshuaige/article/details/79264851
前言
通常在生产环境,我们的每个服务都不会以单节点的方式运行在生产环境,当同一个服务启动多个实例的时候,这些实例都会绑定到同一个消息通道的目标主题(Topic)上。
默认情况下,当生产者发出一条消息到绑定通道上,这条消息会产生多个副本被每个消费者实例接收和处理,但是有些业务场景之下,我们希望生产者产生的消息只被其中一个实例消费,这个时候我们需要为这些消费者设置消费组来实现这样的功能,实现的方式非常简单,我们只需要在服务消费者端设置spring.cloud.stream.bindings.{channel-name}.group属性即可。本章节将来实践验证消费组的具体应用。
本章概要
1、消息回复@SendTo注解使用;
2、消费组验证;
消息回复@SendTo注解使用
为了后续更多功能验证,本小节将实现stream-sender工程。下面将基于sender和receiver工程,通过@SendTo注解实现消息回复功能。
实现stream-sender工程
1、在pom.xml中添加如下依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
2、新建一个启动类,作为一个普通的springboot项目:
package com.cloud.shf.stream;
@SpringBootApplication
public class SernderApp {
public static void main(String[] args) {
SpringApplication.run(SernderApp.class, args);
}
}
3、定义生产消费通道如下:
package com.cloud.shf.stream.sink;
public interface MySink {
/*********************************回复通道******************************/
String REPLAY_SINK_CHANNEL = "replay-sink-channel";
String REPLAY_SOURCE_CHANNEL = "replay-source-channel";
@Input(REPLAY_SINK_CHANNEL)
SubscribableChannel replayInput();
@Output(REPLAY_SOURCE_CHANNEL)
MessageChannel replayOutput();
}
Note:
replay-sink-channel为当前工程监听输入通道;
replay-source-channel为当前工程生产消息输出通道;
4、在application.properties文件中添加如下rabbitmq配置:
server.port=9000
#configure rabbitmq
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=soul
spring.rabbitmq.password=123456
5、定义消息传递对象User实体:
package com.cloud.shf.stream.sink.entity;
public class User implements Serializable {
private static final long serialVersionUID = 695183437612916152L;
private String username;
private int age;
public String getUsername() {
return username;
}
public User setUsername(String username) {
this.username = username;
return this;
}
public int getAge() {
return age;
}
public User setAge(int age) {
this.age = age;
return this;
}
}
6、为了方便消息的生产,采用@InboundChannelAdapter轮询生产消息,每5S生产一个:
package com.cloud.shf.stream.source;
@EnableBinding(value=MySink.class)
public class UserSource {
@Bean
@InboundChannelAdapter(value = MySink.REPLAY_SOURCE_CHANNEL, poller = @Poller(fixedRate = "5000", maxMessagesPerPoll = "1"))
public MessageSource timerMessageSource() {
return () -> new GenericMessage<>(new User().setUsername("shuaishuai").setAge(12));
}
}
7、既然有消息回复,那么必须定义一个监听器:
package com.cloud.shf.stream.sink;
@EnableBinding(value = {MySink.class})
public class SinkReceiver {
/*********************************回复消息监听******************************/
@StreamListener(value = MySink.REPLAY_SINK_CHANNEL)
public void userReceive(@Payload User user) {
LOGGER.info("Received from {} channel age: {}", MySink.REPLAY_SINK_CHANNEL, user.getAge());
}
}
改造stream-receiver工程
1、在MySink中添加sender工程中定义的两个通道,有别于sender工程,两个通道需要互换角色:
package com.cloud.shf.stream.sink;
public interface MySink {
/*********************************回复通道******************************/
String REPLAY_SINK_CHANNEL = "replay-sink-channel";
String REPLAY_SOURCE_CHANNEL = "replay-source-channel";
@Input(REPLAY_SOURCE_CHANNEL)
SubscribableChannel replayInput();
@Output(REPLAY_SINK_CHANNEL)
MessageChannel replayOutput();
}
Note:
replay-sink-channel为当前工程输出通道;
replay-source-channel为当前工程监听输入通道;
2、添加如下对replay-source-channel通道的监听,并对接收用户age+1回复至replay-sink-channel通道:
package com.cloud.shf.stream.sink;
@EnableBinding(value = {Sink.class, MySink.class})
public class SinkReceiver {
/*********************************自动回复消息******************************/
@StreamListener(value = MySink.REPLAY_SOURCE_CHANNEL)
@SendTo(value = {MySink.REPLAY_SINK_CHANNEL})
public User userReplay(@Payload User user) {
LOGGER.info("Received from {} channel age: {}", MySink.REPLAY_SOURCE_CHANNEL, user.getAge());
return user.setAge(user.getAge()+1);
}
}
Note:
@SendTo注解将会将返回值发送至指定通道;
消息回复验证
1、启动receiver、sender工程;
2、查看receiver控制台信息:
3、查看sender控制台信息:
小节:通过最终的日志,可以看到receiver工程从replay-source-channel通道接收到一个消息,即会将更新后的用户信息推送至replay-sink-channel通道,并被sender工程接收消费。
消费组验证
下图即为本小节服务验证的结构图,sender作为消息的生产方,8001、8002服务作为第一组,8003、8004服务作为第二组。
sender工程改造
主要添加一个轮询生产消息的通道,模拟消息生产方。
1、在MySink中添加如下专门测试消费组的通道group-channel:
package com.cloud.shf.stream.sink;
public interface MySink {
/*********************************消费组示例通道******************************/
String GROUP_CHANNEL = "group-channel";
@Output(GROUP_CHANNEL)
MessageChannel groupOutput();
}
2、定义GroupSource轮询往group-channel通道发送消息,每5s发送一个自增一数字:
package com.cloud.shf.stream.source;
@EnableBinding(value = MySink.class)
public class GroupSource {
private static final Logger LOGGER = LoggerFactory.getLogger(GroupSource.class);
private static int count = 0;
@Bean
@InboundChannelAdapter(value = MySink.GROUP_CHANNEL, poller = @Poller(fixedRate = "5000", maxMessagesPerPoll = "1"))
public MessageSource groupMessageSource() {
return () -> {
count++;
LOGGER.info("send {}", count);
return new GenericMessage<>(count);
};
}
}
自此sender工程即完成改造。
receiver工程改造
消息消费端将实现对group-channel通道的监听,并打印接收的消息体。
1、在MySink中添加如下专门测试消费组的通道group-channel:
package com.cloud.shf.stream.sink;
public interface MySink {
/*********************************消费组示例通道******************************/
String GROUP_CHANNEL = "group-channel";
@Input(GROUP_CHANNEL)
SubscribableChannel groupInput();
}
2、添加group-channel通道的监听:
package com.cloud.shf.stream.sink;
@EnableBinding(value = {Sink.class, MySink.class})
public class SinkReceiver {
/*********************************消费组示例******************************/
@Value("${spring.profiles.active:0}")
private String active;
@StreamListener(value = MySink.GROUP_CHANNEL)
public void groupReceiver(@Payload String payload) {
LOGGER.info("Received-{} from {} channel payload: {}", active,MySink.GROUP_CHANNEL, payload);
}
}
Note:
添加了一个动态参数active,此参数将获取当前服务对应的profile值;
在打印的log中打印active以便区分服务(后续服务将通过spring.profiles.active启动多个实例);
3、添加多个profile对应的application-{}.properties配置文件,如下:
其中
application-1.properties配置如下:
spring.profiles.active=1
server.port=8001
spring.cloud.stream.bindings.group-channel.group=receiver-group-1
application-2.properties配置如下:
spring.profiles.active=2
server.port=8002
spring.cloud.stream.bindings.group-channel.group=receiver-group-1
application-3.properties配置如下:
spring.profiles.active=3
server.port=8003
spring.cloud.stream.bindings.group-channel.group=receiver-group-2
application-4.properties配置如下:
spring.profiles.active=4
server.port=8004
spring.cloud.stream.bindings.group-channel.group=receiver-group-2
Note:
每个实例采用不同的http端口;
通过spring.cloud.stream.bindings.{channel-name}.group配置,8001、8002分在receiver-group-1消费组;8003、8004分在receiver-group-2消费组;
4、通过--spring.profiles.active=1|2|3|4依次启动4个receiver实例,并启动sender服务,此时观察各服务的控制台log如下:
sender服务(第一条消息未被截取图片):
8001服务:
8002服务:
8003服务:
8004服务:
5、从rabblitMQ控制台的Exchange看板同样可以得到对应的分析结果:
当启用分组时,group-channel通道对应的bindings如下,就是上述定义的两个分组:
当不进行分组配置时,group-channel通道对应的bindings会出现如下四个,其实就是每个实例都完全隔离:
而此处我们看到的每一个binding后面都对应了一个具体的queue,exchange生成了多个消息副本发送至绑定的每个queue中。故而很容易理解,为何分组后不会被重复消息。
小节:基于上述的log和rabblitmq的控制台可以分析得到如下结论:
8001和8002服务由于分在一组,故会以轮询的方式接收sender服务发出的消息,保证了消费组内不会重复消费;8003和8004服务同理;
由于8003、8004服务组和8001、8002服务组属于并列存在的关系,故并不会互相有所影响;
在源码org.springframework.cloud.stream.binder.rabbit.provisioning.RabbitExchangeQueueProvisioner中有具体对group的操作,其会根据group参数是否配置决定后续的相关细节,如上述控制台bindings名称的由来正如下面的逻辑:
private static final AnonymousQueue.Base64UrlNamingStrategy ANONYMOUS_GROUP_NAME_GENERATOR
= new AnonymousQueue.Base64UrlNamingStrategy("anonymous.");
..........
public ConsumerDestination provisionConsumerDestination(String name, String group, ExtendedConsumerProperties properties) {
boolean anonymous = !StringUtils.hasText(group);
String baseQueueName = anonymous ? groupedName(name, ANONYMOUS_GROUP_NAME_GENERATOR.generateName())
: groupedName(name, group);
...........
}
总结
本章主要验证了如下两个细节功能特性的使用:
消息回复@SendTo注解使用;
消费组验证;
建议在生产环境中,对高可用的多实例服务务必添加spring.cloud.stream.bindings.{channel-name}.group配置,避免消息被重复消费;
————————————————
版权声明:本文为CSDN博主「帅天下」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/songhaifengshuaige/article/details/79264851