使用 Spring Cloud Stream 构建消息驱动微服务
相关源码: spring cloud demo
微服务的目的: 松耦合
事件驱动的优势:高度解耦
Spring Cloud Stream 的几个概念
Spring Cloud Stream is a framework for building message-driven microservice applications.
官方定义 Spring Cloud Stream 是一个构建消息驱动微服务的框架。
应用程序通过 inputs 或者 outputs 来与 Spring Cloud Stream 中binder 交互,通过我们配置来 binding ,而 Spring Cloud Stream 的 binder 负责与中间件交互。所以,我们只需要搞清楚如何与 Spring Cloud Stream 交互就可以方便使用消息驱动的方式
Binder
Binder 是 Spring Cloud Stream 的一个抽象概念,是应用与消息中间件之间的粘合剂。目前 Spring Cloud Stream 实现了 Kafka 和 Rabbit MQ 的binder。
通过 binder ,可以很方便的连接中间件,可以动态的改变消息的
destinations(对应于 Kafka 的topic,Rabbit MQ 的 exchanges),这些都可以通过外部配置项来做到。
甚至可以任意的改变中间件的类型而不需要修改一行代码。
Publish-Subscribe
消息的发布(Publish)和订阅(Subscribe)是事件驱动的经典模式。Spring Cloud Stream 的数据交互也是基于这个思想。生产者把消息通过某个 topic 广播出去(Spring Cloud Stream 中的 destinations)。其他的微服务,通过订阅特定 topic 来获取广播出来的消息来触发业务的进行。
这种模式,极大的降低了生产者与消费者之间的耦合。即使有新的应用的引入,也不需要破坏当前系统的整体结构。
Consumer Groups
“Group”,如果使用过 Kafka 的童鞋并不会陌生。Spring Cloud Stream 的这个分组概念的意思基本和 Kafka 一致。
微服务中动态的缩放同一个应用的数量以此来达到更高的处理能力是非常必须的。对于这种情况,同一个事件防止被重复消费,只要把这些应用放置于同一个 “group” 中,就能够保证消息只会被其中一个应用消费一次。
Durability
消息事件的持久化是必不可少的。Spring Cloud Stream 可以动态的选择一个消息队列是持久化,还是 present。
Bindings
bindings 是我们通过配置把应用和spring cloud stream 的 binder 绑定在一起,之后我们只需要修改 binding 的配置来达到动态修改topic、exchange、type等一系列信息而不需要修改一行代码。
基于 RabbitMQ 使用
以下内容源码: spring cloud demo
消息接收
Spring Cloud Stream 基本用法,需要定义一个接口,如下是内置的一个接口。
public interface Sink {
String INPUT = "input";
@Input("input")
SubscribableChannel input();
}
注释 @Input 对应的方法,需要返回 SubscribableChannel ,并且参入一个参数值。
这就接口声明了一个 binding 命名为 “input” 。
其他内容通过配置指定:
spring:
cloud:
stream:
bindings:
input:
destination: mqTestDefault
destination:指定了消息获取的目的地,对应于MQ就是 exchange,这里的exchange就是 mqTestDefault
@SpringBootApplication
@EnableBinding(Sink.class)
public class Application {
// 监听 binding 为 Sink.INPUT 的消息
@StreamListener(Sink.INPUT)
public void input(Message<String> message) {
System.out.println("一般监听收到:" + message.getPayload());
}
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
定义一个 class (这里直接在启动类),并且添加注解@EnableBinding(Sink.class) ,其中 Sink 就是上述的接口。同时定义一个方法(此处是 input)标明注解为@StreamListener(Processor.INPUT) ,方法参数为 Message 。
启动后,默认是会创建一个临时队列,临时队列绑定的exchange为 “mqTestDefault”,routing key为 “#”。
所有发送 exchange 为“mqTestDefault” 的MQ消息都会被投递到这个临时队列,并且触发上述的方法。
以上代码就完成了最基本的消费者部分。
消息发送
消息的发送同消息的接受,都需要定义一个接口,不同的是接口方法的返回对象是MessageChannel,下面是 Spring Cloud Stream 内置的接口:
public interface Source {
String OUTPUT = "output";
@Output("output")
MessageChannel output();
}
这就接口声明了一个 binding 命名为 “output” ,不同于上述的 “input”,这个binding 声明了一个消息输出流,也就是消息的生产者。
spring:
cloud:
stream:
bindings:
output:
destination: mqTestDefault
contentType: text/plain
contentType:用于指定消息的类型。具体可以参考 spring cloud stream docs
destination:指定了消息发送的目的地,对应 RabbitMQ,会发送到 exchange 是 mqTestDefault 的所有消息队列中。
代码中调用:
@SpringBootApplication
@EnableBinding(Source.class)
public class Application implements CommandLineRunner {
@Autowired
@Qualifier("output")
MessageChannel output;
@Override
public void run(String... strings) throws Exception {
// 字符串类型发送MQ
System.out.println("字符串信息发送");
output.send(MessageBuilder.withPayload("大家好").build());
}
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
通过注入MessageChannel的方式,发送消息。
通过注入Source 接口的方式,发送消息。 具体可以查看样例
以上代码就完成了最基本的生产者部分。
自定义消息发送接收
自定义接口
Spring Cloud Stream 内置了两种接口,分别定义了 binding 为 “input” 的输入流,和 “output” 的输出流,而在我们实际使用中,往往是需要定义各种输入输出流。使用方法也很简单。
interface OrderProcessor {
String INPUT_ORDER = "inputOrder";
String OUTPUT_ORDER = "outputOrder";
@Input(INPUT_ORDER)
SubscribableChannel inputOrder();
@Output(OUTPUT_ORDER)
MessageChannel outputOrder();
}
一个接口中,可以定义无数个输入输出流,可以根据实际业务情况划分。上述的接口,定义了一个订单输入,和订单输出两个 binding。
使用时,需要在 @EnableBinding 注解中,添加自定义的接口。
使用 @StreamListener 做监听的时候,需要指定OrderProcessor.INPUT_ORDER
spring:
cloud:
stream:
defaultBinder: defaultRabbit
bindings:
inputOrder:
destination: mqTestOrder
outputOrder:
destination: mqTestOrder
如上配置,指定了 destination 为 mqTestOrder 的输入输出流。
分组与持久化
上述自定义的接口配置中,Spring Cloud Stream 会在 RabbitMQ 中创建一个临时的队列,程序关闭,对应的连接关闭的时候,该队列也会消失。而在实际使用中,我们需要一个持久化的队列,并且指定一个分组,用于保证应用服务的缩放。
只需要在消费者端的 binding 添加配置项 spring.cloud.stream.bindings.[channelName].group = XXX 。对应的队列就是持久化,并且名称为:mqTestOrder.XXX。
rabbitMQ routing key 绑定
用惯了 rabbitMQ 的童鞋,在使用的时候,发现 Spring Cloud Stream 的消息投递,默认是根据 destination + group 进行区分,所有的消息都投递到 routing key 为 “#‘’ 的消息队列里。
如果我们需要进一步根据 routing key 来进行区分消息投递的目的地,或者消息接受,需要进一步配,Spring Cloud Stream 也提供了相关配置:
spring:
cloud:
stream:
bindings:
inputProductAdd:
destination: mqTestProduct
group: addProductHandler # 拥有 group 默认会持久化队列
outputProductAdd:
destination: mqTestProduct
rabbit:
bindings:
inputProductAdd:
consumer:
bindingRoutingKey: addProduct.* # 用来绑定消费者的 routing key
outputProductAdd:
producer:
routing-key-expression: '''addProduct.*''' # 需要用这个来指定 RoutingKey
spring.cloud.stream.rabbit.bindings.[channelName].consumer.bindingRoutingKey
指定了生成的消息队列的routing keyspring.cloud.stream.rabbit.bindings.[channelName].producer.routing-key-expression 指定了生产者消息投递的routing key
DLX 队列
DLX 作用
DLX:Dead-Letter-Exchange(死信队列)。利用DLX, 当消息在一个队列中变成死信(dead message)之后,它能被重新publish到另一个Exchange,这个Exchange就是DLX。消息变成死信一向有一下几种情况:
消息被拒绝(basic.reject/ basic.nack)并且requeue=false
消息TTL过期(参考:RabbitMQ之TTL(Time-To-Live 过期时间))
队列达到最大长度
DLX也是一个正常的Exchange,和一般的Exchange没有区别,它能在任何的队列上被指定,实际上就是设置某个队列的属性,当这个队列中有死信时,RabbitMQ就会自动的将这个消息重新发布到设置的Exchange上去,进而被路由到另一个队列,可以监听这个队列中消息做相应的处理。
Spring Cloud Stream 中使用
spring.cloud.stream.rabbit.bindings.[channelName].consumer.autoBindDlq=true
spring.cloud.stream.rabbit.bindings.[channelName].consumer.republishToDlq=true
配置说明,可以参考 spring cloud stream rabbitmq consumer properties
结论
Spring Cloud Stream 最大的方便之处,莫过于抽象了事件驱动的一些概念,对于消息中间件的进一步封装,可以做到代码层面对中间件的无感知,甚至于动态的切换中间件,切换topic。使得微服务开发的高度解耦,服务可以关注更多自己的业务流程。
相关文档
使用 Spring Cloud Stream 构建消息驱动微服务的更多相关文章
- Spring Cloud Alibaba学习笔记(12) - 使用Spring Cloud Stream 构建消息驱动微服务
什么是Spring Cloud Stream 一个用于构建消息驱动的微服务的框架 应用程序通过 inputs 或者 outputs 来与 Spring Cloud Stream 中binder 交互, ...
- 「 从0到1学习微服务SpringCloud 」08 构建消息驱动微服务的框架 Spring Cloud Stream
系列文章(更新ing): 「 从0到1学习微服务SpringCloud 」01 一起来学呀! 「 从0到1学习微服务SpringCloud 」02 Eureka服务注册与发现 「 从0到1学习微服务S ...
- SpringCloud之Spring Cloud Stream:消息驱动
Spring Cloud Stream 是一个构建消息驱动微服务的框架,该框架在Spring Boot的基础上整合了Spring Integrationg来连接消息代理中间件(RabbitMQ, Ka ...
- 消息驱动微服务:Spring Cloud Stream
最近在学习Spring Cloud的知识,现将消息驱动微服务:Spring Cloud Stream 的相关知识笔记整理如下.[采用 oneNote格式排版]
- 今天介绍一下自己的开源项目,一款以spring cloud alibaba为核心的微服务架构项目,为给企业与个人提供一个零开发基础的微服务架构。
LaoCat-Spring-Cloud-Scaffold 一款以spring cloud alibab 为核心的微服务框架,主要目标为了提升自己的相关技术,也为了给企业与个人提供一个零开发基础的微服务 ...
- Spring Cloud Stream如何处理消息重复消费?
最近收到好几个类似的问题:使用Spring Cloud Stream操作RabbitMQ或Kafka的时候,出现消息重复消费的问题.通过沟通与排查下来主要还是用户对消费组的认识不够.其实,在之前的博文 ...
- Spring cloud stream【消息分区】
在上篇文章中我们给大家介绍了Stream的消息分组,可以实现消息的重复消费的问题,但在某些场景下分组还不能满足我们的需求,比如,同时有多条同一个用户的数据,发送过来,我们需要根据用户统计,但是消息 ...
- Spring cloud stream【消息分组】
上篇文章我们简单的介绍了stream的使用,发现使用还是蛮方便的,但是在上个案例中,如果有多个消息接收者,那么消息生产者发送的消息会被多个消费者都接收到,这种情况在某些实际场景下是有很大问题的,比 ...
- spring cloud 入门,看一个微服务框架的「五脏六腑」
Spring Cloud 是一个基于 Spring Boot 实现的微服务框架,它包含了实现微服务架构所需的各种组件. 注:Spring Boot 简单理解就是简化 Spring 项目的搭建.配置.组 ...
随机推荐
- mybatis出现NoSuchMethodException异常
今天在idea中调试项目(ssm搭建的项目)的时候,mybatis突然出现了NoSuchMethodException异常,具体的异常时: java.lang.NoSuchMethodExceptio ...
- 剑指offer(1)
题目: 在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序.请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数. ...
- 解决spring多线程不共享事务的问题
在一个事务中使用多线程操作数据库时,若同时存在对数据库的读写操作,可能出现数据读取的不准确,因为多线程将不会共享同一个事务(也就是说子线程和主线程的事务不一样),为了解决这个问题,可以使用spring ...
- HJ212 CRC 16 (C#)
算法 CRC16 校验寄存器赋值为 0xFFFF: 取被校验串的第一个字节赋值给临时寄存器: 临时寄存器与 CRC16 校验寄存器的高位字节进行"异或"运算,赋值给 CRC16 校 ...
- rediret 加/与不加/的区别
- css 媒体查询 注意点
1, 媒体查询表达式之间还可以用逗号,@media (max-width:800px), print 它表示或的意思 @media (max-width: 800px) OR print; 2, n ...
- servlet篇 之 生命周期
二:Servlet的生命周期 背景知识: servlet是单例,在web项目运行期间,一个servlet只会创建一个对象[tomcat帮我们实例 化][尽量不要在servlet中定义成员变量].因为w ...
- Linux命令归纳
Linux基本命令 Linux Xshell远程连接 ssh 用户名@id地址 例如: ssh root@192.168.11.53 增加类指令 创建文件夹 mkdir 文件名 mkdir -p 路径 ...
- Jquery根据滚动条显示返回按钮
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8& ...
- 自己实现strchr函数与strstr函数
char* my_strchr(char* str, int i) { if (NULL == str) { return NULL; } while ('\0' != *str && ...