Stream进阶篇-消费组实现验证

前言
通常在生产环境,我们的每个服务都不会以单节点的方式运行在生产环境,当同一个服务启动多个实例的时候,这些实例都会绑定到同一个消息通道的目标主题(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后面都对应了一个具体的 queueexchange生成了多个消息副本发送至绑定的每个 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 配置,避免消息被重复消费;
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值