本文内容翻译自官方文档,spring-cloud-stream docs,对 Spring Cloud Stream的应用入门介绍。
一、Spring Cloud Stream 简介
官方定义 Spring Cloud Stream 是一个构建消息驱动微服务的框架。
Spring Cloud Stream构建在SpringBoot之上,提供了Kafka,RabbitMQ等消息中间件的个性化配置,引入了发布订阅、消费组和分区的语义概念,有效的简化了上层研发人员对MQ使用的复杂度,让开发人员更多的精力投入到核心业务的处理。
在实际开发过程中,服务与服务之间通信经常会使用到消息中间件,而以往使用了哪个中间件比如RabbitMQ,那么该中间件和系统的耦合性就会非常高,如果我们要替换为Kafka那么变动会比较大,使用Spring Cloud Stream来整合我们的消息中间件,可以降低系统和中间件的耦合性。
二、Spring Cloud Stream 解决什么问题
无感知的使用消息中间件
Stream解决了开发人员无感知的使用消息中间件的问题,因为Stream对消息中间件的进一步封装,可以做到代码层面对中间件的无感知。
中间件和服务的高度解耦
Spring Cloud Stream进行了配置隔离,只需要调整配置,开发中可以动态的切换中间件(如rabbitmq切换为kafka),使得微服务开发的高度解耦,服务可以关注更多自己的业务流程。
三、核心概念和应用模型
主要概念
Spring Cloud Stream 为各大消息中间件产品提供了个性化的自动化配置实现,引用了发布-订阅、消费组、分区的三个核心概念。
Spring Cloud Stream提供了很多抽象和基础组件来简化消息驱动型微服务应用。包含以下内容:
- Spring Cloud Stream的应用模型
- 绑定抽象
- 持久化发布/订阅支持
- 消费者组支持
- 分片支持(Partitioning Support)
- 可插拔API
应用模型
Spring Cloud Stream由一个中立的中间件内核组成。Spring Cloud Stream会注入输入和输出的channels,应用程序通过这些channels与外界通信,而channels则是通过一个明确的中间件Binder与外部brokers连接。
各大消息中间件的绑定抽象
Spring Cloud Stream提供对Kafka,Rabbit MQ,Redis,和Gemfire的Binder实现。Spring Cloud Stream还包括了一个TestSupportBinder,TestSupportBinder预留一个未更改的channel以便于直接地、可靠地和channels通信。
集成Kafka
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-kafka</artifactId>
</dependency>
集成RabbitMQ
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
分区支持
Spring Cloud Stream支持在一个应用程序的多个实例之间数据分区,在分区的情况下,物理通信介质(例如,topic代理)被视为多分区结构。一个或多个生产者应用程序实例将数据发送给多个消费应用实例,并保证共同的特性的数据由相同的消费者实例处理。
Spring Cloud Stream提供了一个通用的抽象,用于统一方式进行分区处理,因此分区可以用于自带分区的代理(如kafka)或者不带分区的代理(如rabbiemq)
分区在有状态处理中是一个很重要的概念,其重要性体现在性能和一致性上,要确保所有相关数据被一并处理,例如,在时间窗平均计算的例子中,给定传感器测量结果应该都由同一应用实例进行计算。
四、编程模型
Spring Cloud Stream提供了一些预定义的注解,用于绑定输入和输出channels,以及如何监听channels。
通过@EnableBinding触发绑定
将@EnableBinding注解添加到应用的配置类,就可以把一个spring应用转换成Spring Cloud Stream应用,@EnableBinding注解本身就包含@Configuration注解,会触发Spring Cloud Stream 基本配置。
@Import(...)
@Configuration
@EnableIntegration
public @interface EnableBinding {
...
Class<?>[] value() default {};
}
@Input 与 @Output
一个Spring Cloud Stream应用可以有任意数目的input和output通道,后者通过@Input和@Output注解在接口中定义。
@StreamListener
定义在方法中,被修饰的方法注册为消息中间件上数据流的事件监听器,注解中属性值对应了监听的消息通道名。
Source,Sink和Processor
Spring Cloud Stream提供了三个开箱即用的预定义接口。Source用于有单个输出(outbound)通道的应用。
public interface Source {
String OUTPUT = "output";
@Output(Source.OUTPUT)
MessageChannel output();
}
Sink用于有单个输入(inbound)通道的应用。
public interface Sink {
String INPUT = "input";
@Input(Sink.INPUT)
SubscribableChannel input();
}
Processor用于单个应用同时包含输入和输出通道的情况。
public interface Processor extends Source, Sink {
}
五、Stream极简实例
下面是一个非常简单的 SpringBootApplication应用,通过依赖Spring Cloud Stream,从Input通道监听消息然后返回应答到Output通道,只要添加配置文件就可以应用。
@SpringBootApplication
@EnableBinding(Processor.class)
public class MyLoggerServiceApplication {
public static void main(String[] args) {
SpringApplication.run(MyLoggerServiceApplication.class, args);
}
@StreamListener(Processor.INPUT)
@SendTo(Processor.OUTPUT)
public LogMessage enrichLogMessage(LogMessage log) {
return new LogMessage(String.format("[1]: %s", log.getMessage()));
}
}
下面解释下这个示例中相关注解的应用:
@EnableBinding声明了这个应用程序绑定了2个通道:INPUT和OUTPUT。这2个通道是在接口Processor中定义的(Spring Cloud Stream默认设置)。所有通道都是配置在一个具体的消息中间件或绑定器中。
@StreamListener(Processor.INPUT)表明这里在input中提取消息,并且处理。
@SendTo(Processor.OUTPUT)表明在output中返回消息。
总结
这篇文章根据Spring Cloud Stream的官方文档,对Stream做了一个整体的介绍,包括设计目标,应用场景,业务模型以及对外开放的注解,下面是我通过一个实例,演示 Spring Cloud Stream 的应用。
stream+rabbitMQ
1. 建module改pom
<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>
<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.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>
2. 改yml
server:
port: 8801
spring:
application:
name: cloud-stream-provider
cloud:
stream:
binders: # 在此处配置要绑定的rabbitMQ的服务信息
defaultRabbit: # 表示定义的名称,用于binding的整合
type: rabbit # 消息中间件类型
environment: # 设置rabbitMQ的相关环境配置
spring:
rabbitmq:
host: 192.168.14.101 #我的rabbitmq装在了虚拟机所有是虚拟机的主机地址
port: 5672
username: guest
password: guest
bindings: # 服务的整合处理
output: # 这个名字是一个通道的名称
destination: studyExchange # 表示要使用的exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设为text/plain
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
eureka:
client:
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
instance:
lease-renewal-interval-in-seconds: 2 # 设置心跳的间隔时间,默认30
lease-expiration-duration-in-seconds: 5 # 超过5秒间隔,默认90
instance-id: send-8801.com # 主机名
prefer-ip-address: true # 显示ip
3. 主启动
@SpringBootApplication
public class StreamMQMain8801 {
public static void main(String[] args) {
SpringApplication.run(StreamMQMain8801.class,args);
}
}
4. 业务类
service层
service接口
public interface IMessageProvider {
public String send();
}
service实现类
import com.zhutianlu.springcloud.service.IMessageProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.MessageBuilder;
import java.util.UUID;
@EnableBinding(Source.class) //定义消息的推送管道
public class MessageProviderImpl implements IMessageProvider {
/**
* //消息发送通道
*/
@Autowired
private MessageChannel output;
@Override
public String send() {
String serial = UUID.randomUUID().toString();
output.send(MessageBuilder.withPayload(serial).build());
System.out.println("*****serial***" + serial);
return serial;
}
}
controller层
import com.zhutianlu.springcloud.service.IMessageProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class SendMessageController {
@Autowired
private IMessageProvider messageProvider;
@GetMapping("/sendMessage")
public String send() {
return messageProvider.send();
}
}
-
启动rabbit默认用户名密码都是guest 地址:http://192.168.14.101:15672/ 格式为主机名加端口号
如果是本地启动为http://localhost:15672/ -
启动8801启动完成之后看rabbit 的Exchange上是否出现studyExchange如果出现则表示成功
-
自此消息推送方8801完成
配置消息订阅方8802
1.建module改pom此处pom和8801完全一致
2.改pom
server:
port: 8802
spring:
application:
name: cloud-stream-consumer
cloud:
stream:
binders: # 在此处配置要绑定的rabbitMQ的服务信息
defaultRabbit: # 表示定义的名称,用于binding的整合
type: rabbit # 消息中间件类型
environment: # 设置rabbitMQ的相关环境配置
spring:
rabbitmq:
host: 192.168.14.101
port: 5672
username: guest
password: guest
bindings: # 服务的整合处理
input: # 这个名字是一个通道的名称
destination: studyExchange # 表示要使用的exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设为text/plain
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
#group: zhutianluA # 不同的组存在重复消费,相同的组之间竞争消费。
eureka:
client:
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
instance:
lease-renewal-interval-in-seconds: 2 # 设置心跳的间隔时间,默认30
lease-expiration-duration-in-seconds: 5 # 超过5秒间隔,默认90
instance-id: receive-8802.com #主机名
prefer-ip-address: true # 显示ip
3.主启动
@SpringBootApplication
public class StreamMQMain8802 {
public static void main(String[] args) {
SpringApplication.run(StreamMQMain8802.class,args);
}
}
4.业务类
controller
@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);
}
}
为了待会儿消息重复消费的问题和持久化的问题我们再次直接在建一个8803和8802完全一致改一下端口号即可
- 启动7001、7002
- 启动8801、8802、8803
- 访问http://localhost:8801/sendMessage点两次即可
- 我们会发现8802和8803都接收到了8801发送的两条消息,如果这是在订单模块的话后果不堪设想,所以我们要避免消息重复消费的行为出现
避免消息重复消费
在此我们用分组实现避免消息重复消费的发生
把8802和8803中的
#group: zhutianluA # 不同的组存在重复消费,相同的组之间竞争消费。
注释打开即可 表示8802和8803在同一个分组zhutianluA
导致原因
rabbitmq默认分组group是不同的,组流水号不一样,被认为在不同的组,可以重复消费
(同一个组存在竞争关系只有其中一个可以被消费,不同组不存在这种关系所以会被重复消费)
消息持久化
如果服务器宕机或者重启,如果此微服务添加了分组则可以被持久化在此微服务死机的时间内发布的消息依然会被订阅到如果没有则不可以,
在此我们也是用分组实现消息持久化的