微服务之Springcloud 从零基础到入门——Stream篇
一. 背景简介
在微服务的开发过程中,服务与服务之间通信经常会使用到消息中间件,而如果确定使用了某个消息中间件比如RabbitMQ,那么该中间件和系统的耦合性就会十分的高,假如现在要把RabbitMQ替换为Kafka,那么产生的变动会特别的大,许多地方都需要进行配置。这时我们可以使用Spring Cloud Stream来整合我们的消息中间件,来降低系统和中间件的耦合性。作用就是屏蔽底层的消息中间件的差异,降低切换成本,统一消息的编程模型。对于消息系统,实际上最初的 SUN 公司是非常看中的,所以在 EJB 的时代里面专门提供有消息驱动 Bean(Message Driven Bean、MDB)利用消息驱动 Bean 可以进行消息的处理操作。利用消息驱动 bean 的模式可以简化用户的操作复杂度,直接传递一些各类的数据即可实现业务的处理操作。SpringCloudStream 就是实现了 MDB 功能。
二. Stream简介
应用程序通过 inputs 或者 outputs 来与 Spring Cloud Stream 中binder 交互,通过我们配置来 binding(绑定) ,而 Spring Cloud Stream 的 binder 负责与消息中间件交互。input对应于消息消费者,output对应于消息生产者。所以,我们只需要搞清楚如何与 Spring Cloud Stream 交互就可以方便使用消息驱动的方式。
Stream通过使用Spring Integration来连接消息代理中间件以实现消息事件驱动。Spring Cloud Stream 为一些供应商的消息中间件产品提供了个性化的自动化配置实现,引用了发布-订阅、消费组、分区的三个核心概念。目前仅支持RabbitMQ、Kafka。
组成 | 说明 |
---|---|
Middleware | 中间件,目前只支持RabbitMQ和Kafka |
Binder | Binder是应用与消息中间件之间的封装,目前实行了Kafka和RabbitMQ的Binder,通过Binder可以很方便的连接中间件,可以动态的改变消息类型(对应于Kafka的topic,RabbitMQ的exchange),这些都可以通过配置文件来实现 |
@Input | 注解标识输入通道,通过该输入通道接收到的消息进入应用程序 |
@Output | 注解标识输出通道,发布的消息将通过该通道离开应用程序 |
@StreamListener | 监听队列,用于消费者的队列的消息接收 |
@EnableBinding | 指信道channel和exchange绑定在一起 |
三. Stream消息驱动案例
- 创建消息生产者
- 引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</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>
- 修改application.yml配置
server:
port: 8506
spring:
application:
name: cloud-stream-provider
cloud:
stream:
binders:
defaultRabbit: # 定义的绑定对象
type: rabbit # 绑定对象rabbitmq的类型
environment: # 绑定对象rabbitmq的环境配置
spring:
rabbitmq:
host: 192.168.192.129
port: 5672
username: root
password: root
bindings:
output:
destination: MyExchange # 约定的通道名称
contentType: application/json # 设置消息的类型为json,也可设置为文本类型
binder: defaultRabbit # 引用定义好的绑定对象
eureka:
client:
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
instance:
lease-renewal-interval-in-seconds: 2 # 设置心跳的间隔时间
lease-expiration-duration-in-seconds: 5
instance-id: 8506.com
prefer-ip-address: true
- 定义消息发送接口
public interface SendService {
String send();
}
- 定义消息发送实现类
@EnableBinding(Source.class) //定义消息推送管道
public class SendServiceImpl implements SendService {
@Resource
private MessageChannel output; //消息发送通道
@Override
public String send() {
String uuid= UUID.randomUUID().toString();
//发送消息
output.send(MessageBuilder.withPayload(uuid).build());
String msg="uuid="+uuid;
return msg;
}
}
- 定义测试消息发送的controller类
@RestController
public class SendController {
@Resource
private SendService sendService;
@GetMapping(value = "send")
public String send(){
return sendService.send();
}
}
- 定义主启动类
@SpringBootApplication
public class Stream8506Application {
public static void main(String[] args) {
SpringApplication.run(Stream8506Application.class,args);
}
}
- 测试
在浏览器输入http://localhost:8506/send,可以看到在控制台输出的发送消息,并且可以在RabbitMQ的管理界面看到Exhange中的Mychange。
- 创建消息消费者
- 引入依赖
此处依赖同消息生产者 - 修改application.yml配置文件
server:
port: 8606
spring:
application:
name: cloud-stream-consumer
cloud:
stream:
binders:
defaultRabbit: # 定义的绑定对象
type: rabbit # 绑定对象的类型
environment: # 绑定对象的环境配置
spring:
rabbitmq:
host: 192.168.192.129
port: 5672
username: root
password: root
bindings:
input:
destination: MyExchange # 表示要使用的通道名称
contentType: application/json # 设置消息的类型为json,也可设置为文本类型
binder: defaultRabbit # 引用定义好的绑定对象
eureka:
client:
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
instance:
lease-renewal-interval-in-seconds: 2 # 设置心跳的间隔时间
lease-expiration-duration-in-seconds: 5
instance-id: 8606.com
prefer-ip-address: true
- 定义消息的监听程序类
@EnableBinding(Sink.class)
@Component
public class ReceiveController {
@Value("${server.port}")
private String port;
@StreamListener(Sink.INPUT)
public void get(Message<String> message){
System.out.println("消费者:"+message.getPayload()+",port="+port);
}
}
- 主启动类
@SpringBootApplication
public class Stream8606Application {
public static void main(String[] args) {
SpringApplication.run(Stream8606Application.class,args);
}
}
- 测试
浏览器输入http://localhost:8506/send,消费者的控制台输出发送的消息
四. 分组消费
- 重复消费问题的产生
若一个通道的消费者有两个的话,那么这两个消费者会获取到同一个消息并将其消费,那么就会产生问题。若在实际的微服务调用中,若发送的消息为订单的消息,并且被两个服务消费掉,那么会使得一个订单被消费两次,与实际情况不符。 - 解决方案
分组消费,在stream中,在同一个组之下的消费者是竞争关系,消息只会被消费一次。那么可以将消费特定消息的消费者放到同一个组下,这样可以保证消息只会被其中的一个服务消费,与该消费者同在mygroup下的消费者会采用轮询的机制去消费消息。在消息的消费者中添加组的配置如下:
spring:
cloud:
stream:
bindings:
input:
destination: MyExchange
contentType: application/json
binder: defaultRabbit
group: mygroup # 配置分组防止重复消费
五. 消息持久化
- 持久化的原因
如果没有消息持久化,万一消息的消费方发生了宕机事件,那么当消费者再次启动的后也不能获取得到消息中间件得消息,造成了消息丢失。 - 如何做消息持久化
消费者配置了分组就默认配置了消息持久化,即便消费者宕机,那么再次重启后还能消费消息中间件里边的消息。
六. 下一篇介绍
本文简要介绍了Stream的基本用法,接下来将对spring cloud alibaba的技术进行介绍。