SPRING CLOUD微服务实战笔记--消息驱动的微服务:Spring Cloud Stream

刺骨的言语ヽ痛彻心扉 2022-02-25 19:28 441阅读 0赞

Spring Cloud Stream

  • 快速入门
  • 核心概念
    • 绑定器
    • 发布-订阅模式
    • 消费组
    • 消息分区
  • 使用详解
    • 绑定消息通道
      • 注入绑定接口
      • 注入消息通道
    • 消费组与消息分区
    • 消息类型
  • 绑定器详解
    • 多绑定器配置
    • RabbitMQ与Kafka绑定器

快速入门

  • 创建一个基础的Spring Boot工程,命名为stream-hello
  • 编辑pom.xml,引入Spring Cloud Stream对RabbitMQ的支持



    org.springframework.cloud
    spring-cloud-starter-stream-rabbit
    1.3.4.RELEASE


    org.springframework.boot
    spring-boot-starter-web



    org.springframework.boot
    spring-boot-starter-test
    test

  • 创建用于接收来自RabbitMQ消息的消费者SinkReceiver,具体如下:

    @EnableBinding(Sink.class)
    public class SinkReceiver {

    1. private static Logger logger = LoggerFactory.getLogger(HelloApplication.class);
    2. @StreamListener(Sink.INPUT)
    3. public void receive(Object payload){
    4. logger.info("Received: " + payload);
    5. }

    }

  • 创建应用主类,同其他Spring Boot一样,不需要修改(可以直接生成demo)
    启动RabbitMQ以及Spring Boot应用

  • 登录http://localhost:15672/,进入RabbitMQ的管理界面,然后进入Queues查看到Consumers中增加了一个消费者
    RabbitMQ
  • Publish message中发布一条信息,用于测试
    RabbitMQ publish message
  • 这时可以看到SpringBoot应用的控制台中打印出以下信息:

    Received: [B@7236cac6

刚才我们在RabbitMQ管理界面发布的信息,由SinkReceiver来消费了
说一下定义在SinkReceiver的Spring Cloud Stream的核心注解
@EnableBinding:该注解用来指定一个或多个定义了@Input或@Output注解的接口,以此来实现对消息通道(Channel)的绑定。
@StreamListener:该注解主要定义在方法上,作用是将被修饰的方法注册为消息中间上数据流的事件监听器,注解中属性值对应了监听的消息通道名。
第一个注解主要用来将当前类SinkReceiver绑定到Sink.class上,而Sink.class已经绑定input通道,第二个注解主要监听input通道上的数据

核心概念

Spring Cloud Stream构建的应用程序与消息中间件之间是通过绑定器Binder相关联的,绑定器对于应用程序而言起到了隔离作用,它使得不同消息中间件的实现细节对应用程序来说是透明的。如下图所示,绑定器是作为通道和消息中间件之间的桥梁进行通信
Binder

绑定器

绑定器与数据库中的DataSource相同,在连接数据库时有很多数据库厂家,每个厂家的实现数据源细节都不尽相同,所以将数据源抽象成DataSource接口,就可以将具体实现细节进行隔离。此处的绑定器与其有相同的效果,由于各消息中间件构建的初衷不同,在实现细节上也有很大差异。通过定义绑定器作为中间层,完美地实现了应用程序与消息中间件细节之间的隔离。
通过向应用程序暴露统一的Channel通道,不需要再考虑各种不同的消息中间件的实现。当需要更换其他消息中间件时,要做的就是更换它们对应的Binder绑定器而不需要修改任何SpringBoot的应用逻辑。

发布-订阅模式

Spring Cloud Stream中的消息通信方式遵循了发布-订阅模式,当一条消息被投递到消息中间件之后,它会通过共享的Topic主题进行广播,消息消费者在订阅的主题中收到它并触发自身的业务逻辑处理。
生产者 生产消息发布在shared topic(共享主题)上,然后 消费者 通过订阅这个topic来获取消息
发布订阅模式
在RabbitMQ中,Topic对应Exchange,在Kafka中对应Kafka中的Topic

消费组

在现实的微服务架构中,每一个微服务应用为了实现高可用和负载均衡,实际上都会部署多个实例。
如果在同一个主题上的应用需要启动多个实例时,可以通过spring.cloud.stream.bindings.input.group属性为应用指定一个组名,这样一个组里只有一个成员真正接收到消息并进行处理。
消费组

消息分区

对于同一条消息,多次到达之后可能是由不同的实例进行消费的。
对于一些场景,需要对一些具有相同特征的消息设置每次都被同一个消费实例处理
分区概念的引入就是为了解决这样的问题:当生产者将消息数据发送给多个消费者实例时,保证拥有共同特征的消息数据始终是由同一个消费者实例接收和处理。

使用详解

@EnableBinding注解用来创建消息通道的绑定

  1. @Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE})
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Documented
  4. @Inherited
  5. //注解后会成为Spring的基本配置类
  6. @Configuration
  7. //加载Spring Cloud Stream运行需要的几个基础配置类
  8. @Import({ BindingServiceConfiguration.class, BindingBeansRegistrar.class, BinderFactoryConfiguration.class, SpelExpressionConverterConfiguration.class})
  9. @EnableIntegration
  10. public @interface EnableBinding {
  11. Class<?>[] value() default { };
  12. }

绑定消息通道

注入绑定接口

  • 创建一个将Input消息通道作为输出通道的接口,具体如下:
    注:书中对于@Output(Sink.INPUT),应该修改为@Output(Source.OUTPUT)

    @Component
    public interface SinkSender {

    1. @Output(Source.OUTPUT)
    2. MessageChannel output();

    }

  • 对快速入门中定义的SinkReceiver做修改:在@EnableBinding注解中增加对SinkSender的指定,使Spring Cloud Stream能创建出对应的实例

    @EnableBinding(value = { Sink.class, SinkSender.class})
    public class SinkReceiver {

    1. private static Logger logger = LoggerFactory.getLogger(SinkSender.class);
    2. @StreamListener(Sink.INPUT)
    3. public void receive(Object payload){
    4. logger.info("Received: " + payload);
    5. }

    }

  • 修改application.properties,添加输出通道和输入通道名称

    spring.cloud.stream.bindings.input.destination=raw-sensor-data
    spring.cloud.stream.bindings.output.destination=raw-sensor-data

  • 创建一个单元测试类,通过@Autowired注解注入SinkSender实例,用来发送消息

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringBootTest
    @WebAppConfiguration
    public class HelloApplicationTests {

    1. @Autowired
    2. private SinkSender sinkSender;
    3. @Test
    4. public void contextLoads() {
    5. sinkSender.output().send(MessageBuilder.withPayload("From SinkSender").build());
    6. }

    }

  • 运行主类和测试类,可以发现控制台输出内容
    输出内容

注入消息通道

在测试类中加入下面内容,用于注入消息通道

  1. @Autowired
  2. private MessageChannel input;
  3. @Test
  4. public void contextLoads1(){
  5. input.send(MessageBuilder.withPayload("From MessageChannel").build());
  6. }
  • 运行主类和测试类,控制台输出内容Received:From MessageChannel

消费组与消息分区

1.消费组
有些业务场景下,希望生产者产生的消息只被一个实例消费,这个时候就需要为这些消费者设置消费组来实现这样的功能

  • 先实现一个消费者应用SinkReceiver,实现greetings主题上的输入通道绑定

    @EnableBinding(value = { Sink.class})
    public class SinkReceiver {

    1. private static Logger logger = LoggerFactory.getLogger(SinkReceiver.class);
    2. @StreamListener(Sink.INPUT)
    3. public void receive(User user){
    4. logger.info("Received: " + user);
    5. }

    }

  • 为了将SinkReceiver的输入通道目标设置为greetings主题,以及将该服务的实例设置为同一个消费组,可做如下设置:

    spring.cloud.stream.bindings.input.group=Service-A
    spring.cloud.stream.bindings.input.destination=greetings

这其中spring.cloud.stream.bindings.input.group属性指定了该应用实例都属于Service-A消费组,而spring.cloud.stream.bindings.input.destination属性则指定了输入通道对应的主题名

  • 完成消息消费者应用之后,再实现一个消息生产者应用SinkSender

    @EnableBinding(value = { Source.class})
    public class SinkSender {

    1. private static Logger logger = LoggerFactory.getLogger(SinkSender.class);
    2. @Bean
    3. @InboundChannelAdapter(value = Source.OUTPUT,poller = @Poller(fixedDelay = "2000"))
    4. public MessageSource<String> timerMessageSource(){
    5. return ()->new GenericMessage<>("{\"name\":\"didi\",\"age\":30}");
    6. }

    }

  • 为消息生产者SinkSender的配置做些改动,使得生产者的输出通道绑定目标也指向greetings主题

    spring.cloud.stream.bindings.output.destination=greetings

启动多个消费者实例,再启动生产者实例,通过输出可以看到生产者发出的消息会被启动的消费者以轮询的方式进行接收和输出
消费者交替
2.消息分区
消费组只是保证了同一消息只被一个消费者实例进行接收和处理,但是不能保证消息总能被同一个实例进行消费。这个时候需要对消息进行分区处理。

  • 在消费者应用SinkReceiver中,对配置文件进行一些修改,具体如下:

    spring.cloud.stream.bindings.input.group=Service-A
    spring.cloud.stream.bindings.input.destination=greetings
    spring.cloud.stream.bindings.input.consumer.partitioned=true
    spring.cloud.stream.instance-count=2
    spring.cloud.stream.instance-index=0

spring.cloud.stream.bindings.input.consumer.partitioned:通过该参数开启消费者分区功能
spring.cloud.stream.instanceCount:该参数指定了当前消费者的总实例数量
spring.cloud.stream.instanceIndex:该参数设置当前实例的索引号,从0开始,最大值为spring.cloud.stream.instanceCount参数-1。

  • 在生产者应用SinkSender中,对配置文件也做一些修改,具体如下:

    spring.cloud.stream.bindings.output.producer.partitionKeyExpression=payload
    spring.cloud.stream.bindings.output.producer.partitionCount=2

spring.cloud.stream.bindings.output.producer.partitionKeyExpression:通过该参数指定了分区键的表达式规则,我们可以根据实际的输出消息规则SpEL来生成合适的分区键
spring.cloud.stream.bindings.output.producer.partitionCount:该参数指定了消息分区的数量
spring.cloud.stream.bindings.output.producer.partition-key-expression=1 表示只有分区ID为1的消费端能接收到信息。
spring.cloud.stream.bindings.output.producer.partition-key-expression=0 表示只有分区ID为0的消费端能接收到信息。

  • 测试
    spring.cloud.stream.bindings.output.producer.partition-key-expression设置为0, 可以看到左边的实例序列号为0的实例在不停地接收消息
    实例0接收消息
    通过RabbitMQ的管理页面也能看到只有序号为0的实例在接收消息,同时也能看到实例1处于空闲状态
    实例0在运行

消息类型

目前,Spring Cloud Stream中自带支持了以下几种常用的消息类型转换:

  • JSON与POJO的互相转换
  • JSON与org.springframework.tuple.Tuple的互相转换
  • Object与byte[]的互相转换。为了实现远程传输序列化的原始字节,应用程序需要发送byte类型的数据,或是通过实现Java的序列化接口来转换为字节(Object对象必须可序列化)
  • String与byte[]的互相转换
  • Object向纯文本的转换:Object需要实现toString()方法

绑定器详解

多绑定器配置

在一个应用程序中使用多个绑定器时,往往其中一个绑定器会是主要使用的,而第二个可能是为了适应一些特殊要求(比如性能等原因)。可以先通过设置默认绑定器来为大部分的通道设置绑定器。比如,使用RabbitMQ设置默认绑定器:

  1. spring.cloud.stream.defaultBinder=rabbit

在设置默认绑定器之后,再为其他一些少数的消息通道单独设置绑定器,比如:

  1. spring.cloud.stream.bindings.input.binder=kafka

当需要在一个应用程序中使用同一类型不同环境的绑定器时,可以通过配置实现通道绑定。

  1. spring.cloud.stream.bindings.input.binder=rabbit1
  2. spring.cloud.stream.bindings.input.binder=rabbit2
  3. #rabbit1的通道类型为rabbit
  4. spring.cloud.stream.binders.rabbit1.type=rabbit
  5. spring.cloud.stream.binders.rabbit1.environment.spring.rabbitmq.host=192.168.0.101
  6. spring.cloud.stream.binders.rabbit1.environment.spring.rabbitmq.port=5672
  7. #rabbit2的通道类型为rabbit
  8. spring.cloud.stream.binders.rabbit2.type=rabbit
  9. spring.cloud.stream.binders.rabbit2.environment.spring.rabbitmq.host=192.168.0.102
  10. spring.cloud.stream.binders.rabbit2.environment.spring.rabbitmq.port=5672

从上面的配置中,可以看出rabbit1和rabbit2的通道类型都为rabbit,只是主机地址不同。
当采用显示配置方式时会自动禁用默认的绑定器配置,所以当定义了显示配置以后,对于这些绑定器的配置需要通过spring.cloud.stream.binders.属性来进行设置。对于绑定器的配置 主要有下面4个参数:

  • spring.cloud.stream.binders.<configurationName>.type:指定了绑定器的类型,可以是rabbit,kafka或者其他自定义绑定器的标识名。
  • spring.cloud.stream.binders.<configurationName>.environment:参数可以用来设置各绑定器的属性,默认为空
  • spring.cloud.stream.binders.<configurationName>.inheritEnvironment:参数用来配置当前绑定器是否继承应用程序自身的环境配置,默认为true
  • spring.cloud.stream.binders.<configurationName>.defaultCandidate:参数用来设置当前绑定器配置是否被视为默认绑定器的候选项,默认为true,当需要让当前配置不影响默认配置时,可以将该属性设置为false

RabbitMQ与Kafka绑定器

RabbitMQ与Kafka的绑定器是如何使用消息中间件中不同概念来实现消息的生产与消费的:

  • RabbitMQ绑定器:在RabbitMQ中,通过Exchange交换器来实现Spring Cloud Stream的主题概念,所以消息通道的输入输出目标映射了一个具体的Exchange交换器。而对每个消费组,则会为对应的Exchange交换器绑定一个Queue队列来进行消息收发。
  • Kafka绑定器:由于Kafka自身就有Topic的概念,所以SpringCloudStream的主题直接采用Kafka的Topic主题概念,每个消费组的通道目标都会直接连接Kafka的主题进行消息收发。

发表评论

表情:
评论列表 (有 0 条评论,441人围观)

还没有评论,来说两句吧...

相关阅读