Spring Cloud Stream 介绍
Spring Cloud Stream 官方文档、中文指导手册
Spring Cloud Stream是一个构建消息驱动微服务的框架。
为什么用 Stream?
比方说我们用到了RabbitMQ和Kafka,由于这两个消息中间件的架构上的不同,像RabbitMQ有Exchange
,kafka有Topic
和Partitions
分区。
这些中间件的差异性导致我们实际项目开发给我们造成了一定的困扰,我们如果用了两个消息队列的其中一种,后面的业务需求,我想往另外一种消息队列进行迁移,这时候无疑就是一个灾难性的,一大堆东西都要重新推倒重新做,因为它跟我们的系统耦合了,这时候Spring Cloud Stream给我们提供了—种解耦合的方式。
Stream 凭什么可以统一底层差异?
在没有绑定器这个概念的情况下,我们的Spring Boot应用要直接与消息中间件进行信息交互的时候,由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性。通过定义绑定器 Binder作为中间层,完美地实现了应用程序与消息中间件细节之间的隔离。通过向应用程序暴露统一的Channel通道,使得应用程序不需要再考虑各种不同的消息中间件实现。所以,我们只需要搞清楚如何与Spring Cloud Stream交互就可以方便使用消息驱动的方式。
应用程序通过 inputs
或者 outputs
来与Spring Cloud Stream中 binder
对象交互。
Binder:
inputs
:对应于消费者
outputs
:对应于生产者
Stream 中的消息通信方式遵循了 发布-订阅模式。使用Topic主题进行广播:
- 在RabbitMQ就是
Exchange
- 在Kakfa中就是
Topic
Stream 常用注解
Stream 三个重要组成部分:
- Binder:很方便的连接中间件,屏蔽差异。
- Channel:通道,是队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过Channel对队列进行配置。
- Source 和 Sink:简单的可理解为参照对象是Spring Cloud Stream自身,从Stream发布消息就是输出,接受消息就是输入。
编码 API 和常用注解
- Middleware:中间件,目前只支持RabbitMQ和Kafka
- Binder:Binder是应用与消息中间件之间的封装,目前实行了Kafka和RabbitMQ的Binder,通过Binder可以很方便地连接中间件,可以动态的改变消息类型(对应于Kafka的topic,RabbitMQ的exchange),这些都可以通过配置文件来实现
- @Input:注解标识输入通道,通过该输入通道接收到的消息进入应用程序
- @Output:注解标识输出通道,发布的消息将通过该通道离开应用程序
- @StreamListener:监听队列,用于消费者的队列的消息接收
- @EnableBinding:指信道channel和exchange绑定在一起
Stream 实战
工程中新建三个子模块
cloud-stream-rabbitmq-provider8801
:消息生产者
cloud-stream-rabbitmq-consumer8802
:消息消费者
cloud-stream-rabbitmq-consumer8803
:消息消费者
Stream 生产者模块
新建Module:cloud-stream-rabbitmq-provider8801
- 导入 Maven 依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>cloud2021</artifactId> <groupId>com.zhao.springcloud</groupId> <version>1.0.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion>
<artifactId>cloud-stream-rabbitmq-provider8801</artifactId>
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-stream-rabbit</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
</project>
|
- 配置文件(注意需要在spring下单独配置RabbitMQ的信息):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| server: port: 8801
spring: application: name: cloud-stream-provider
rabbitmq: host: 192.168.1.203 port: 5672 username: admin password: admin
cloud: stream: binders: defaultRabbit: type: rabbit environment: spring: rabbitmq: host: 192.168.1.203 port: 5672 username: admin password: admin bindings: output: destination: studyExchange content-type: application/json binder: defaultRabbit
eureka: client: service-url: defaultZone: http://localhost:7001/eureka instance: lease-renewal-interval-in-seconds: 2 lease-expiration-duration-in-seconds: 5 instance-id: send-8801.com prefer-ip-address: true
|
- 主启动类
StreamMQMain8801
1 2 3 4 5 6 7 8 9
| import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication public class StreamMQMain8801 { public static void main(String[] args) { SpringApplication.run(StreamMQMain8801.class,args); } }
|
- 业务类:发送消息接口
IMessageProvider
:
1 2 3
| public interface IMessageProvider { public String send(); }
|
- 发送消息接口实现类
MessageProviderImpl
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import com.zhao.springcloud.service.IMessageProvider; import org.springframework.cloud.stream.annotation.EnableBinding; import org.springframework.cloud.stream.messaging.Source; import org.springframework.integration.support.MessageBuilder; import org.springframework.messaging.MessageChannel;
import javax.annotation.Resource; import java.util.UUID;
@EnableBinding(Source.class) public class MessageProviderImpl implements IMessageProvider { @Resource private MessageChannel output;
@Override public String send() { String serial = UUID.randomUUID().toString(); output.send(MessageBuilder.withPayload(serial).build()); System.out.println("*****serial: "+serial); return null; } }
|
- Controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import com.zhao.springcloud.service.IMessageProvider; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController public class SendMessageController { @Resource private IMessageProvider messageProvider;
@GetMapping(value = "/sendMessage") public String sendMessage() { return messageProvider.send(); } }
|
Stream 消费者模块
新建Module:cloud-stream-rabbitmq-consumer8802
- 导入 Maven 依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>cloud2021</artifactId> <groupId>com.zhao.springcloud</groupId> <version>1.0.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion>
<artifactId>cloud-stream-rabbitmq-consumer8802</artifactId> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-stream-rabbit</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
</project>
|
- 配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| server: port: 8802
spring: application: name: cloud-stream-consumer
rabbitmq: host: 192.168.1.203 port: 5672 username: admin password: admin
cloud: stream: binders: defaultRabbit: type: rabbit environment: spring: rabbitmq: host: 192.168.1.203 port: 5672 username: admin password: admin bindings: output: destination: studyExchange content-type: application/json binder: defaultRabbit
eureka: client: service-url: defaultZone: http://localhost:7001/eureka instance: lease-renewal-interval-in-seconds: 2 lease-expiration-duration-in-seconds: 5 instance-id: send-8801.com prefer-ip-address: true
|
- 主启动类
StreamMQMain8802
:
1 2 3 4 5 6 7 8 9
| import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication public class StreamMQMain8802 { public static void main(String[] args) { SpringApplication.run(StreamMQMain8802.class,args); } }
|
- 业务类使用
@EnableBinding(Sink.class)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.stream.annotation.EnableBinding; import org.springframework.cloud.stream.annotation.StreamListener; import org.springframework.cloud.stream.messaging.Sink; import org.springframework.messaging.Message; import org.springframework.stereotype.Component;
@Component @EnableBinding(Sink.class) public class ReceiveMessageListenerController { @Value("${server.port}") private String serverPort;
@StreamListener(Sink.INPUT) public void input(Message<String> message) { System.out.println("消费者1号,----->接受到的消息: "+message.getPayload()+"\t port: "+serverPort); } }
|
Stream 消息重复消费
依照服务消费者8802,克隆出来一份运行 8803 - cloud-stream-rabbitmq-consumer8803
。
依次启动各个服务:
- RabbitMQ 服务端
- 服务注册中心 - 7001
- 消息生产者 - 8801
- 消息消费者 - 8802
- 消息消费者 - 8803
测试:
解决方案:设置分组group
生产实际案例
比如在如下场景中,订单系统我们做集群部署,都会从RabbitMQ中获取订单信息,那如果一个订单同时被两个服务获取到,那么就会造成数据错误,我们得避免这种情况。这时我们就可以使用Stream中的消息分组来解决。
注意在Stream中处于同一个group中的多个消费者是竞争关系,就能够保证消息只会被其中一个应用消费一次。不同组是可以全面消费的(重复消费)。
设置分组解决消息重复消费
微服务应用放置于同一个分组group中,就能够保证消息只会被其中一个应用消费一次。不同的组是可以重复消费的,同一个组内会发生竞争关系,只有其中一个可以消费默认情况下8802和8803处于不同的分组,因此会发生消息重复消费。
解决方案:将8802/8803变成相同组:group: A_Group
。分别修改8802和8803两个模块的配置文件,添加group
属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| spring: application: name: cloud-stream-provider cloud: stream: binders: defaultRabbit: type: rabbit environment: spring: rabbitmq: host: localhost port: 5672 username: guest password: guest bindings: output: destination: studyExchange content-type: application/json binder: defaultRabbit group: A_Group
|
将二者的group
属性均设置为 A_Group
。此时配置的两个微服务属于相同的组,即8802/8803实现了轮询分组,每次只有一个消费者,8801模块发的消息只能被8802或8803其中一个接收到,这样就避免了重复消费。
结论:同一个组的多个微服务实例,每次只会有一个拿到