文章实例使用的Spring Cloud版本为Finchley.SR1,Spring Boot版本为2.0.4。
1 Spring Cloud Stream
Spring Cloud Stream是一个用于构建消息驱动的微服务应用的框架,其提供的一系列抽象屏蔽了不同类型消息中间件使用上的差异,同时也大大简化了Spring在整合消息中间件时的使用复杂度。
1.1 Spring Cloud Stream应用模型
应用程序通过Channel(消息通道,分为inputs和outputs)与外界进行通信,而Channel通过特定的消息中间件Binder(Spring Cloud Stream提供实现,并使用Spring Boot的自动化进行配置,但目前只支持了 Kafka、Rabbit MQ等几种有限的消息中间件)连接到具体的中间件,这样的应用模型使得不同消息中间件的实现细节对于应用而言是透明的,因而在开发过程中,我们只需要通过使用Channel来统一进行消息的传递,不再需要关注具体使用的消息中间件类型,从而更加专注于业务的开发。
另外,Spring Cloud Stream应用程序之间的消息通信遵循发布 - 订阅模型,当一条消息被投递到消息中间件之后,它会通过共享的Topic主题进行广播,订阅了该主题的消息消费者均可以收到这条消息。
2 示例
以下示例消息中间件使用的是RabbitMQ。
2.1 消息监听
新建Spring Boot项目,实现消息的消费,这里命名为stream-receiver,并引入Spring Cloud Stream依赖:
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Finchley.SR1</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-test-support</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
application.properties配置:
server.port=9090
#RabbitMQ配置
spring.rabbitmq.host=
spring.rabbitmq.port=5672
spring.rabbitmq.username=
spring.rabbitmq.password=
spring.rabbitmq.virtual-host=/
#指定input通道绑定RabbitMQ的TopicExchange名称
spring.cloud.stream.bindings.input.destination=stream-start
绑定输入通道,并启用:
@EnableBinding(Sink.class)
public class MessageReceiver {
@StreamListener(Sink.INPUT)
public void receiver(Message<String> message){
System.out.println( "接收到消息:" + message.getPayload());
}
}
这里的Sink是Spring Cloud Stream中默认实现的对输入消息通道绑定的定义,另外还有输出通道Source和包含了输入和输出通道的Processor。
启动应用,登录RabbitMQ管理页面,在Exchanges标签页我们可以看到新建了一个名为stream-start,类型为topic的Exchange,并且其绑定了一个名为stream-start.anonymous.acf8lsQ-TH6t8hS7LDPprA
的队列(Queue);转到Queue标签页,我们在这条stream-start.anonymous.acf8lsQ-TH6t8hS7LDPprA
队列上发布一条任意消息,可以看到我们的Spring Cloud Stream应用正常地接收到的这条消息。
另外,我们观察到,这里的exchange名称与我们在配置文件中指定的spring.cloud.stream.bindings.input.destination
值一致,如果我们不指定这个值的话,那么Spring Cloud Stream将创建一个与通道名称一致的exchange,即是"input"。
2.2 消费组
通常,我们会希望通过启动多个实例来实现应用的扩展,在这种情况下,这些实例会监听着同一个主题,如果有消息产生,那么消息将会被同一个服务的多个实例重复消费,为避免这种情况,就需要用到消费组了。
消费者通道配置可以使用该spring.cloud.stream.bindings.<channelName>.group
属性来指定群组名称,这时同一个应用程序如果启动了多个实例,那么这些不同实例将置于竞争的使用者关系中,其中只有一个实例需要处理给定的消息。
示例:
修改上一个例子的消息消费者,application.properties增加配置:
#指定input通道消费组名称
spring.cloud.stream.bindings.input.group=stream-consumer
新建一个消息生产者项目,命名为stream-sender,引入依赖(与消费者一致)后,配置application.properties:
server.port=9091
spring.rabbitmq.host=
spring.rabbitmq.port=5672
spring.rabbitmq.username=
spring.rabbitmq.password=
spring.rabbitmq.virtual-host=/
#指定output通道绑定的RabbitMQ的TopicExchange名称
spring.cloud.stream.bindings.output.destination=stream-start
绑定输出通道,并启用:
@EnableBinding(Source.class)
public class MessageSender {
@Autowired
private Source source;
public void send(String message){
source.output().send(MessageBuilder.withPayload(message).build());
}
}
添加Controller测试:
@RestController
public class MeesageController {
@Autowired
private MessageSender messageSender;
@PostMapping("/send")
public String send(@RequestParam String message){
messageSender.send(message);
return "";
}
}
启动两个stream-receiver,并启动stream-sender,通过/send
请求发送消息,我们可以看到,一条消息只会被一个实例收到并消费。
来到RabbitMQ的管理页面,切换到Queues标签,可以看到,多个实例共享一个队列,队列名为stream-start.stream-consumer
,可以看出其命名形式是destination-消费组名。
2.3 分区
Spring Cloud Stream支持在给定应用程序的多个实例之间对数据进行分区,使用分区,我们可以确保,在同一个消费组中,具有共同特征标识的数据将由同一个消费者实例处理。
示例:
首先修改消息生产者stream-sender,增加application.properties配置:
#根据消息头部的whichPart输出消息分区Key表达式
spring.cloud.stream.bindings.output.producer.partitionKeyExpression=headers['whichPart']
#分区数
spring.cloud.stream.bindings.output.producer.partitionCount=2
其中,spring.cloud.stream.bindings.output.producer.partitionKeyExpression
的值是一个SpEL表达式,Spring Cloud Stram将根据这个表达式对生产的消息进行计算,以提取每个消息的partitionKey
(分区键),有了partitionKey后
,再默认根据key.hashCode() % partitionCount
计算出消息要发往的分区。
我们修改一下消息发送程序,给发送的每个消息加上一个whichPart
header再发送:
@EnableBinding(Source.class)
public class MessageSender {
@Autowired
private Source source;
public void send(String message){
int whichPart = new Random().nextInt(2);
System.out.println("发送消息:" + message + ",发往分区:" + whichPart);
source.output().send(MessageBuilder.withPayload(message).setHeader("whichPart", whichPart).build());
}
}
再修改消费者stream-receiver,先增加application.properties配置:
#开启分区
spring.cloud.stream.bindings.input.consumer.partitioned=true
spring.cloud.stream.instance-count=2
#标识每个应用实例对应的分区
spring.cloud.stream.instance-index=0
再修改下监听程序,打印出当前实例设置的分区:
@EnableBinding(Sink.class)
public class MessageReceiver {
@Value("${spring.cloud.stream.instance-index}")
private int partition;
@StreamListener(Sink.INPUT)
public void receiver(Message<String> message){
System.out.println("分区" + partition + ",接收到消息:" + message.getPayload());
}
}
启动stream-sender,再分别指定spring.cloud.stream.instance-index
参数值为0、1启动两个stream-receiver实例。成功启动后,我们通过/send
接口任意发送几个请求后,观察控制台可以得知:header被设置whichPart为0的消息被分区索引为0的stream-receiver实例消费了,header被设置whichPart为1的消息被分区索引为1的stream-receiver实例消费了。
再次来到RabbitMQ的管理页面,切换到Queues标签,可以看到创建了两条不同的队列,分别是stream-start.stream-consumer-0
和stream-start.stream-consumer-1
,不难看出,此前一个消费组共用一条队列,现在被以分区索引号标识分成了多条队列,并由不同的Routing Key绑定到了exchange。
示例代码:GitHub