简介
springcloudstream是啥?它与JMS有何区别?如何使用?它可以理解为对JMS更为易用、实用的进一步抽象与包装的产品,JMS定义的点对点、发布订阅模型它都支持。它借鉴了IO流的设计思想,从而设计了输入流Input、输出流Output为介质来发送和接收消息。它不关心谁来发送消息、持久化消息等等,全部交给其称为Middleware的消息中间件来处理即可,只需要绑定到任何一种消息中间件,即可实现系统之间通过异步消息进行交互,默认支持的消息中间件有rabbitmq、kafka。其继承了springboot的优秀设计,切换消息中间件只需要修改依赖和配置信息即可完成,对业务代码完全无侵入。
本文将介绍其基本原理,介绍如何在实际工作中使用springcloudstream,内容有
- springcloudstream设计模型
- 如何绑定消息中间件rabbitmq
- Input、Output如何理解与使用
- Input、Output如何关联,对应的队列名是啥?
- 如何利用Output发送消息
- 如何利用Input关联StreamListener接收并处理消息
- 如何实现点对点消息,消息分组?
- 如何实现消息发布订阅模型?
- 消息消费失败如何故障转移与重试?
- @RereshScope与@StreamListener擦出的火花,如何避坑?
原文:传送门
注:本文基于springcloud2.1.3 Greenwich.RELEASE 版本
1、springcloudstream设计模型
2、如何绑定到消息中间件rabbitmq
- 引入maven依赖
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
- 配置rabbitmq服务器连接信息
spring:
cloud:
stream:
binders:#配置多个binder
defaultRabbit: #配置binder的名字
type: rabbit #类型
environment:
spring:
rabbitmq: #rabbitmq服务器连接信息
host: localhost
port: 5672
username: admin
password: admin
virtual-host: /
defaultBinder: defaultRabbit #默认绑定器
default: #默认配置,所有通道可以使用此配置
content-type: application/json #消息的格式,此处其沿用了springmvc里的messageConverter,跟接口的ContentType配置类似
binder: defaultRabbit #使用默认的绑定器
bindings: #针对单个Input或者Output单独进行配置,优先级高于默认配置
queueName:
destination: testQueue #topic的名字
content-type: application/json #消息的格式,此处其沿用了springmvc里的messageConverter,跟接口的ContentType配置类似
binder: defaultRabbit #使用默认的绑定器
group: serviceA #配置同一个应用的实例属于同一个组,消息只消费一次
- 如果只引入了一个消息中间件,可以采用如下简约配置即可
spring:
rabbitmq:
host: localhost
port: 5672
username: admin
password: admin
3、 Input、Output如何理解与使用
- Input : 应用内用来接收消息的通道
- Output : 应用内用来发送消息的通道
- 创建Input和Output
//应用serviceA定义消息接收通道
public interface MessageInputChannel {
@Input("testQueue")
public SubscribableChannel messageInput();
}
//应用serviceB定义消息发送通道
public interface MessageOutputChannel {
@Output("testQueue")
public MessageChannel messageOutput();
}
- 绑定通道和应用
- 应用serviceA如下
@SpringBootApplication
@EnableBinding(MessageInputChannel.class)
public class StartServiceAApplication {
public static void main(String[] args) {
SpringApplication.run(StartServiceAApplication.class, args);
}
//接收消息
@StreamListener("testQueue")
public void handle(String msg){
System.out.println("收到来自testQueue的消息:"+msg);
}
}
- 应用serviceB如下
@SpringBootApplication
@EnableBinding(MessageOutputChannel.class)
@RestController
public class StartServiceBApplication {
@Autowired
private MessageOutputChannel output;
public static void main(String[] args) {
SpringApplication.run(StartServiceBApplication.class, args);
}
@GetMapping(value="/sendMessage")
public void sendMessage(@RequestParam("msg") String msg){
output.messageOutput()
.send(MessageBuilder.withPayload(msg).build());
}
}
4、Input、Output如何关联,对应的队列名是啥?
- Input默认名为@Input(value = “testQueue”)注解的value值,这里便是:testQueue
- Output默认名为@Output(value = “testQueue”)注解的value值,这里便是:testQueue
- 当然我们也可以通过配置将queue或者topic名进行更换,配置如下:
spring:
cloud:
stream:
bindings:
testQueue:
destination: test #修改topic的名字
同一个项目中既要发送消息、又要接收同一个topic的消息时,就需要为input和output配置别名,因为同一个项目里springcloudstream不允许定义同名的input和output,所以要修改
- 例如:
public interface MessageSingleChannel {
@Input("testQueueInput")
public SubscribableChannel messageInput();
@Output("testQueueOutput")
public MessageChannel messageOutput();
}
- 对应的配置如下:
spring:
cloud:
stream:
bindings:
testQueueInput:
destination: testQueue #配置别名
testQueueOutput:
destination: testQueue #配置别名
这样就把两个通道的topic配置成一样的了,同一应用内也可以使用消息异步驱动业务了
5、如何利用Output发送消息
利用springcloudstream提供的MessageChannel,即可发送消息,示例如下:
output.messageOutput().send(MessageBuilder.withPayload(msg).build());
6、如何利用Input关联StreamListener接收并处理消息
利用StreamListener便可监听消息
//接收简单string消息
@StreamListener("testQueue")
public void handle(String msg){
System.out.println("收到来自testQueue的消息:"+msg);
}
//接收复杂消息
@StreamListener("testQueue1")
public void handle(Map<String,String> map){
System.out.println("收到来自testQueue的消息:"+JSON.toJSONString(map));
}
7、如何实现点对点消息,消息分组与消息的持久化?
为何需要分组?如果不配置分组,springcloudstream默认为所有的input实例分配一个匿名的组名,这样会导致生产者服务发送一条消息,同一项目的每个实例都会各自收到一条相同消息,在一些应用场景,如用户下单,是不允许重复下单的,所以需要将同一个项目的不同实例分配为同一个组,只允许其中一个实例消费消息,进行处理。
开启分组后,rabbitmq默认会开启对消息的持久化能力
- 配置serviceA实例分组
spring:
cloud:
stream:
bindings:
testQueue:
destination: test #配置别名
group: serviceA #配置分组
- 启动多个serviceA的实例,发送消息,测试,可以看到一条消息,只会被一个serviceA实例接收
8、如何实现消息发布订阅模型?
- 跟消息分组不一样的地方就是,有的业务场景,生产者发送一条消息,需要通知所有的监听者服务实例,完成状态的更新或者数据的同步等操作,这个时候就需要使用到发布订阅模型了。
- 如果同一应用的实例不指定分组名,默认都是独立的监听者,自然就满足了发布订阅的要求了,也就不需要做任何配置了
- 如果消息需要持久化,也可为每个实例单独配置不同的组名,也同样可以实现发布订阅模型
- 启动多个serviceA服务,通过serviceB发送消息,可以看到每个实例均收到了该消息
9、消息消费失败如何故障转移与重试?
这里的应用场景是可靠消息的发送与接收,消息需要开启持久化,实现方式有如下两种
- 1、可以开启消息手动确认机制,处理成功后才确认消息
- 2、也可以通过抛出运行时异常,使消息重新回到消息中间件服务中
消息如果多次消费失败,rabbitmq会认为这条消息是有毒的,会将其丢入死信队列,并可以配置告警,通知业务人员排查问题,失败的次数上限可以进行配置
10、@RereshScope与@StreamListener擦出的火花,如何避坑?
学过springcloudconfig的同学应该都知道@RereshScope是用来动态更新组件信息的,如果在一个需要动态刷新配置的类中定义@StreamListener,会出现什么现象呢?
- 错误示例,如下所示
@RereshScope
public class Test {
@Value("${app.username}")
private String username;
//接收消息
@StreamListener("testQueue")
public void handle(String msg){
System.out.println(username+"收到来自testQueue的消息:"+msg);
}
}
这样导致的结果就是,当消息发送过来时,应用会找不到合适的监听器来处理,导致消息消费不了。
为啥会这样呢,是由于当刷新配置信息的时候,你的StreamListener重复注册了同样的监听器,但是之前的监听器所属的类实例却被销毁了,当然找不到该监听器了
- 这里郑重提醒,小伙伴们千万不能这么干,不然遇到这种问题也是很头疼的,一不注意,还真发现不了这个坑。