本文目录
Spring Cloud Stream 是一个构建高扩展和事件驱动的微服务系统的框架,用于连接共有消息系统。这是官方高大上的说法,我的理解就是像Spring Data封装所有数据源访问一样,封装一套对共有消息系统访问的统一框架。在我写文章的这个时候,Spring Cloud Stream 封装了rabbitMq、kafka、kafkaStream、Amazon Kinesis,然后还有Google PubSub、Solace PubSub+、Azure Event Hubs在维护中。这一堆都是些啥?好吧,我也就认识RabbitMQ和kafka。不过,我觉得,这高大上的框架力求统一,那细节肯定会要有所舍弃,也就肯定替代不了产品各自的API。只是,这种封装确实让上手开发变得简单,而且这种统一封装的思想其实是最值得学习的。那就还是用RabbitMQ来一起玩一玩这个牛逼的框架把。示例代码上传到了https://gitee.com/tearwind仓库,有兴趣的去看看。
什么是事件驱动?
事件驱动的本质其实就是观察者模式,将数据的变化以事件的形式广播出去,而不再关注每个相关方。在JDK中,有Observer和Observable来封装观察者模式,而在Spring中也有事件驱动机制,可以很方便的进行系统内的事件驱动。
打个比方,电子商城,有人来买个东西,看着简单,后面一大堆人就要跟着忙乎。记订单、通知库存、发起支付、短信通知、邮件通知…一堆的事情,都要一个个的发起。手忙脚乱,还要担心各个流程是不是出问题。那怎么从这样的混乱中抽身出来呢?那就用事件驱动,把有人下单这个事件,在商城内广播出去,让各个相关服务主动去关注下单这个事件,而不用一个个去通知。这样,就改造成了事件驱动。事件驱动模式给系统增加了很大的可扩展性。某一天,要给买东西的人增加一个推荐相关产品的服务,那不用对已有的下单功能做任何改动,只要增加一个关注下单事件的服务就行了。
Spring Cloud Stream的基础概念
Spring Cloud Stream封装出三个最基础的概念来对各种第三方消息中间件进行统一抽象。
1、Destination Binders:负责集成外部消息系统的组件。
2、Destination Binding:由Binders创建的,负责沟通外部消息系统、消息发送者和消息消费者的桥梁。
3、Message:消息发送者与消息消费者沟通的简单数据结构。
Spring Cloud Stream的简单使用
Srping Cloud Stream封装了一组很简单的框架,只需要很少量的代码就能快速实现对消息中间件的访问。以RabbitMQ为例。
首先、创建一个Maven项目,引入依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
其中,rabbit的spring-cloud-stream支持还有一个包spring-cloud-stream-binder-rabbit,测试了一下,好像没什么区别。实际上他们的github代码库都是在一起的。
而spring-boot-starter-web依赖只是为了简单封装一个测试环境。
然后、定义消息接受者
@Component
@EnableBinding(Sink.class)
public class StreamReceiver {
@EventListener
@StreamListener(Sink.INPUT)
public void process(Object message) {
System.out.println("received message : " + message);
}
}
然后、定义一个http接口来封装一个消息发送者
@RestController
@EnableBinding(Source.class)
public class SendMessageController {
@Autowired
private Source source;
@GetMapping("/send")
public Object send(String message) {
MessageBuilder<String> messageBuilder = MessageBuilder.withPayload(message);
source.output().send(messageBuilder.build());
return "message sended : "+message;
}
}
然后、增加SpringBoot配置文件appLication.properties
spring.cloud.stream.bindings.output.destination=streamExchange
spring.cloud.stream.bindings.input.destination=streamExchange
spring.cloud.stream.bindings.input.group=stream
spring.cloud.stream.bindings.input.content-type=text/plain
然后,配置启动类就可以启动应用,访问本机的RabbitMQ服务了。
@SpringBootApplication
public class RabbitApplication {
public static void main(String[] args) {
SpringApplication.run(RabbitApplication.class, args);
}
}
启动服务后,访问接口http://localhost:8080/send?message=123就能看到后台服务接收到发出来的message消息。
Spring Cloud Stream干了些什么?
非常简单的几行代码就实现了对于RabbitMQ消息中间件的访问与接收。那在这个示例中,Spring Cloud Stream到底干了些什么事情?下面来梳理一下。
1、配置服务器地址
是在SpringBoot的autoconfigure包中,封装了一套关于Rabbit服务器的默认配置,可以查看RabbitProperties这个类。当然,也就可以在application.properties中覆盖这些配置,指向实际的RabbitMQ地址。
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtualHost=/
2、在RabbitMQ中声明Exchange和Queue
查看RabbitMQ的服务器管理页面,能看到程序帮我们声明了一个Topic类型的streamExchange,然后其上面还绑定了一个streamExchange.stream的队列,绑定的路由关键字是#。(RabbitMQ的细节就不多说了,可以看看我的相关博文)
而程序中的消息发送者是将消息发送到streamExchange,然后rabbitMQ将消息转发到StreamExchange.stream队列,消息接收者从队列接收到消息。这个流程,就是Spring Cloud Stream在背后为我们做的事情。
与kafka的交互,SpringCloudStream也在同样的代码封装下,做了类似的事情。他帮我们屏蔽了与消息中间件的交互细节,开发人员甚至都感知不到消息中间件的存在,可以更多的关注消息处理业务细节。实际上,如果要将上面的示例改为访问kafka,代码都不需要动,只需要更换依赖spring-cloud-starter-stream-rabbit,然后改配置文件指向kafka的服务地址就行。
当然,这么简单的使用,肯定是不够的。Spring Cloud Stream封装后,其实还是屏蔽掉了RabbitMQ的一些更多使用细节,当要对RabbitMQ进行深入使用时,还是会暴露出一些水土不服来。那有哪些水土不服?再来继续深挖一下。
深入Spring Cloud Stream
关于配置信息,可以参看https://github.com/spring-cloud/spring-cloud-stream-binder-rabbit,但是这里面很多配置方法我都没看懂。这里整理下我自己了解到的部分。
1、Binder配置
Spring Cloud Stream是通过Binder来定义一个外部消息服务器。而对RabbitMQ来说,这个binder就是exchange的抽象。默认情况下,RabbitMQ的binders使用了SpringBoot的ConnectionFactory,所以,他也支持spring-boot-starter-amqp组件中提供的对RabbitMQ的所有配置信息。这些配置信息在application.properties里都以spring.rabbitmq开头。–关于spring-boot-starter-amqp在gitee示例中也有。可以参考。
而如果要配置多个Binder访问不同的外部消息服务器(如同时访问kafka和rabbitmq),可以通过spring.cloud.stream.binders..environment.=的格式来进行配置。另外,当配置了多个binder时,可以通过spring.cloud.stream.default-binder属性指定默认的binder。
spring.cloud.stream.binders.testbinder.environment.spring.rabbitmq.host=localhost
spring.cloud.stream.binders.testbinder.environment.spring.rabbitmq.port=5672
spring.cloud.stream.binders.testbinder.environment.spring.rabbitmq.username=guest
spring.cloud.stream.binders.testbinder.environment.spring.rabbitmq.password=guest
例如这样的配置就指定了一个名为testbinder的binder。至于官网上说的以spring.cloud.stream.rabbit.binder开头的一些配置,我试了下不行。不知道是不是配置的格式不对,或者是用的版本太低。–Eclipse的STS插件不支持这一类属性的提示。 具体配置参照Spring Cloud Stream Rabbitmq的Github地址: https://github.com/spring-cloud/spring-cloud-stream-binder-rabbit
2、Binding配置
Binding是实际进行消息交互的桥梁,在RabbitMQ中,一个接收消息的binding通常对应一个queue,(当然,在发送消息时,可以直接对应一个exchange)。在Spring cloud stream中,依然是将binding与binder建立绑定关系,然后消息接受者通过binding来接收消息。在Spring Cloud Stream中,需要通过@EnableBinding注解,向容器中注入一个Binding接口对象,而在接口对象中,增加@Input注解指定接收消息的binding,而通过@Output注解指定发送消息的binding。在Spring Cloud Stream中,默认就提供了Source、Sink、Processor三个接口对象,这三个对象就是很简单的接口,当然也可以照样配置自己的binging接口对象。比如Source,其实就是这样定义:
public interface Source {
String OUTPUT = "output";
@Output(Source.OUTPUT)
MessageChannel output();
}
这个@Output注解指定的output,就是声明出来的binding对象名,这个binding就会对应RabbitMQ中的一个queue,Spring Cloud Stream默认是将此声明为一个消息发送队列。
使用@EnableBinding声明了之后,这个binding不需要任何配置就可以@Autowired注入后直接用来发消息。
而如果不加任何配置信息,应用启动应用后,会在RabbitMQ中声明一个默认的exchange和queue。但是这默认的queue名字很长,而且很多细节还不够好用,如果需要定制,当然就需要在配置文件中对这个binding的一些属性进行配置。Sping Cloud Stream中对bingding的配置是以spring.cloud.stream.bindings..=的格式来指定的。例如:
spring.cloud.stream.bindings.output.destination=streamExchange
spring.cloud.stream.bindings.output.group=myoutput
–注意:消息发送者端
这个destination属性在RabbitMQ中就会对应一个exchange,而group则对应一个queue名。通过这样的声明,spring cloud stream就会在RabbitMQ中声明一个streamExchange的exchange(topic类型),同时声明一个名为streamExchange.myoutput的queue,并与streamExchange建立绑定关系(routingKey为#)。这个可以到RabbitMQ的管理页面看到的。
通过该接口发送的消息就会发送到对应的这个streamExchange中里,然后由RabbitMQ转发给其上绑定的queue。至于更多的属性,可以参考下官方的github,上面的配置示例好像都不能直接用,但是可以参考下。例如一般常用的,可以通过 spring.cloud.stream.bindings.output.content-type属性指定发送的消息格式。
当有多个binder时,可以通过属性spring.cloud.stream.bindings..binder手动指定当前Binding绑定那个binder。
3、消费者组配置
SpringCloudStream的分组消费策略
SpringCloudStream的消费分组策略,是想要统一成一种类似于kafka的分组机制来进行分组消费。我们回忆一下,在kafka中的分组策略,是不同的group,都会消费到同样的一份message副本,而在同一个group中,只会有一个消费者消费到一个message。这种分组消费策略,严格来说,在Rabbit中是不存在的。RabbitMQ是通过不同类型的exchange来实现不同的消费策略的。这虽然与kafka的这一套完全不同,但是在SpringCloudStream针对RabbitMQ的实现中,可以很容易的看到kafka这种分组策略的影子。
当有多个消费者实例消费同一个bingding时,Spring Cloud Stream同样是希望将这种分组策略,移植到RabbitMQ中来的。就是在不同的group中,会同样消费同一个Message,而在同一个group中,只会有一个消费者消息到一个Message。
而针对这种消费策略,Spring Cloud Stream还提供定制的配置方式。Spring Cloud Stream中的分组消费策略是这样配置的。
#启动发送者分区
spring.cloud.stream.bindings.output.producer.partitioned=true
#指定参与消息分区的消费端节点数量
spring.cloud.stream.bindings.output.producer.partition-count=2
#只有消费端分区ID为1的消费端能接收到消息
spring.cloud.stream.bindings.output.producer.partition-key-expression=1
而在消息消费者端,也可以指定当前消费者端的分区ID
#启动消费分区
spring.cloud.stream.bindings.input.consumer.partitioned=true
#参与分区的消费端节点个数
spring.cloud.stream.bindings.input.consumer.instance-count=2
#设置该实例的消费端分区ID
spring.cloud.stream.bindings.input.consumer.instance-index=1
启用这个分组策略后,当前这个消费者实例就只会消费奇数编号的消息,而偶数编号的消息则不会发送到这个消费者中。–注意,这并不是说偶数编号的消息不会被消费,只是不会被这个实例消费而已。
这种消费模式,更是RabbitMQ原生所没有的。而实际上,Spring Cloud Stream是通过自己的方式实现的这个分区消费的功能。
实际上,在跟踪查看RabbitMQ的实现时,就会发现,Spring Cloud Stream在增加了消费者端的分区设置后,会对每个有效的分区创建一个单独的queue。如上例中,就会创建名为streamExchange.stream-1的queue(当有分区ID为0的消费者端时,才会创建streamExchange.stream-0这个queue),而发送者端的消息,会最终发送到这个streamExchange.stream-1的队列上,而不是streamExchange.stream上。这样就完成了分区消费。
但是,我在用了一下后发现,这个分区消费的策略在针对RabbitMQ时,反正感觉不是很严谨,当把分区数量和分区ID不按套路分配时,并没有太多的检查和日志信息,但是就是收不到消息。
另外,在@StreamListener注解中也有condition属性可以将消息按照条件进行分配,该属性支持一个SPEL表达式,只接收满足条件的消息。
使用原生消息转发机制
当然,Spring Cloud Stream也可以把消息转发机制交由外部消息中间件自己执行,如果要用RabbitMQ的fanout、worker等模式,就可以自己在RabbitMQ中创建好exchange和queue,然后在Spring Cloud Stream中声明。
#绑定exchange
spring.cloud.stream.binding.<bindingName>.destination=fanoutExchange
#绑定queue
spring.cloud.stream.binding.<bindingName>.group=myQueue
#不自动创建queue
spring.cloud.stream.rabbit.bindings.<bindingName>.consumer.bindQueue=false
#不自动声明exchange(自动声明的exchange都是topic)
spring.cloud.stream.rabbit.bindings.<bindingName>.consumer.declareExchange=false
#队列名只声明组名(前面不带destination前缀)
spring.cloud.stream.rabbit.bindings.<bindingName>.consumer.queueNameGroupOnly=true
#绑定rouytingKey
spring.cloud.stream.rabbit.bindings.<bindingName>.consumer.bindingRoutingKey=myRoutingKey
#绑定exchange类型
spring.cloud.stream.rabbit.bindings.<bindingName>.consumer.exchangeType=<type>
#绑定routingKey
spring.cloud.stream.rabbit.bindings.<bindingName>.producer.routingKeyExpression='myRoutingKey'
4、服务消费者消息转发
在Spring Cloud Stream中服务消费者还将接收到的消息进行消息转发,具体就是在接受者的服务接口上增加**@SendTo**声明,加了该声明后,Spring Cloud Stream会将接收方法的返回值转发到另一个指定的binding中。
5、Message格式
在消息中间件中,一般传递的消息都只是字符串类型,但是Spring Cloud Stream还支持扩展消息的类型,例如,将对象以JSON字符串的格式在消息发送者与消息接收者之间传递。不过在我看来,这玩意用处不大。
spring.cloud.stream.bindings.input.content-type=text/plain
在applicaiton.properties中,可以通过content-type指定消息类型,默认是text/plain,如果需要传递对象,可以改为application/json.
其实这相当于增加了一个消息转换器,在接收到消息时,对消息格式进行一下转换。
6 、rabbit个性化配置
Spring Cloud Stream在通用配置的基础上,也支持各个消息中间件的个性化配置。这部分配置以spring.cloud.stream.rabbit开头,支持对binder、producer、consumer进行单独的配置。例如下面介绍的死信队列、懒加载队列,就是RabbitMQ独有的。具体配置照样是参照官方的github仓库说明。
6.1、Dead letter exchange
在RabbitMQ中有一类专门处理死信(Dead letter)的exchange和queue。当有以下几种情况发生时,就意味着队列中的该消息变成了死信:
1.有消息被拒绝(basic.reject/ basic.nack)并且设置消息不重新返回队列,spring.rabbitmq.listener.default-requeue-rejected=true=false(这个默认的配置是true,就是消息处理失败后会重新返回队列,这时候要注意,如果队列已经满了,就会循环不断的报错,这时候就要考虑死信了)
2.队列达到最大长度
3.消息TTL过期
RabbitMQ的这个机制,可以很好的用来做延迟队列或者消息补发之列的问题。
关于RabbitMQ的死信机制,是在正常队列dlQueue上声明一个死信交换机dlExchange,然后这个死信交换机可以像正常Exchange一样,去绑定队列,分发消息。其实现方式,就是在队列中增加声明几个属性来指定死信交换机。而这几个属性,可以用SpringBoot的方式声明Bean来实现,也可以在SpringCloudStream中通过配置信息指定,甚至,在RabbitMQ管理端建立队列时指定,都是可以的。下面会演示SpringCloudStream配置方式来指定这个属性。其他方式也很容易,就不多说了。
x-dead-letter-exchange: mirror.dlExchange 对应的死信交换机
x-dead-letter-routing-key: mirror.messageExchange1.messageQueue1 死信交换机routing-key
x-message-ttl: 3000 消息过期时间
durable: true 持久化,这个是必须的。
管理端看到的队列消息如下:
例如下面的一组配置,就可以实现从output这个binding发送的消息,经过3秒后,被input这个binding消费到。
spring.cloud.stream.rabbit.bindings.input.destination=DlqExchange
spring.cloud.stream.rabbit.bindings.input.group=dlQueue
spring.cloud.stream.rabbit.bindings.output.destination=messageExchange1
spring.cloud.stream.rabbit.bindings.output.producer.required-groups=messageQueue1
spring.cloud.stream.rabbit.rabbit.bindings.output.producer.autoBindDlq=true
spring.cloud.stream.rabbit.rabbit.bindings.output.producer.ttl=3000
spring.cloud.stream.rabbit.rabbit.bindings.output.producer.deadLetterExchange=DlqExchange
spring.cloud.stream.rabbit.rabbit.bindings.output.producer.deadLetterQueueName=DlqExchange.dlQueue
6.2 lazy Queue
RabbitMQ从3.6.0版本开始引入了惰性队列(Lazy Queue)的概念。惰性队列会尽可能的将消息存入磁盘中,而在消费者消费到相应的消息时才会被加载到内存中,它的一个重要的设计目标是能够支持更长的队列,即支持更多的消息存储。当消费者由于各种各样的原因(比如消费者下线、宕机亦或者是由于维护而关闭等)而致使长时间内不能消费消息造成堆积时,惰性队列就很有必要了。
其具体配置主要是给队列配置一个 “x-queue-mode“” 属性,该属性可取值为 default 和 lazy,默认是 default。然后按照上面的方式给队列加上这个属性就行了。
补充发现的小细节:
示例中,关于Spring Cloud Stream版本,最开始用的是Spring Cloud的Dalston.SR3版本中的1.2.1.RELASE版本。后来又手动指定了新一点的2.2.0.RELEASE版本。 使用时发现,在新版本中,StreamReceiver中定义的消费者,在新版本中可以消费到的消息,不光是RabbitMQ中的消息,还有很多系统内的事件,像AsyncConsumerStartedEvent、ApplicationReadyEvent(springBoot启动事件)、ServletRequestHandledEvent(请求响应事件)等等。这也意味着Spring Cloud Stream逐渐变成了一个高于外部消息中间件的更高一级消息中间件。
总结
Spring Cloud Stream对我们访问消息中间件提供了统一并且简单的封装,可以快速的开发事件驱动的微服务系统。但是,各个消息中间件其实处理方式是有很大区别的,这种统一封装颇有强拧的瓜不甜的味道。这也幸亏是Spring社区的大神出手,一般人出手,估计随便就黄了。以Rabbit为例,在我使用的低版本Spring Cloud中,不支持spring.cloud.stream.rabbit的属性配置,一看就是想要把所有消息中间件的消费模式来个大一统,但是后来版本也支持了spring.cloud.stream.rabbit的属性配置,可以针对rabbit做一些特殊的配置与处理。给我的感觉就是以RabbitMQ为例,如果对消息中间件用得不深,可以用Spring Cloud Stream快速搭建。但是如果要用深一点,感觉要花的功夫比用原生的API或者是spring-cloud-starter-amqp这种针对性开发的包要多得多。