Spring Cloud Stream是统一消息中间件编程模型的框架,屏蔽底层消息中间件的差异,降低学习成本及切换成本,其核心就是对消息中间件进一步封装。官方定义Spring Cloud Stream是一个用于构建基于消息的微服务应用框架。
Spring Cloud Stream的Binder对象概念非常重要,不同的消息中间件产品Binder的实现是不同的。如,Kafka的实现是KafkaMessageChannelBinder
,RabbitMQ的实现是RabbitMessageChannelBinder
,RocketMQ的实现是RocketMQMessageChannelBinder
。(来自官网https://spring.io/projects/spring-cloud-stream),目前支持的消息中间件产品:
还有一个重要概念,Binding
,分为Input Binding
和Output Binding
。通过Binding来绑定消息生产者和消息消费者,构建一座沟通的桥梁。底层使用Binder对象与消息中间件交互。
那么为什么使用Spring Cloud Stream呢?
既然要使用Spring Cloud Stream,那么就需要选择一款消息中间件,这里用RocketMQ。
RocketMQ前身是Metaq,当Metaq发布3.0版本时,更名为RocketMQ,经历淘宝双十一大流量的考验,值得信赖。RocketMQ是一款分布式消息中间件,以下优点:
还是按照对应的版本去下载,笔者使用的spring cloud alibaba
的版本是2021.0.4.0
,那么RocketMQ版本选择4.9.4
即可
下载地址贴到这里https://archive.apache.org/dist/rocketmq/4.9.4/rocketmq-all-4.9.4-bin-release.zip
下载速度过慢,这里提供下网盘资源:
链接:https://pan.baidu.com/s/1cwpVD2to-vkyNMpJ25kEbg
提取码:9609
安装
windows下启动rocketmq时,只需进入bin目录下,点击mqnamesrv.cmd
和mqbroker.cmd
分别启动Name Server和Broker服务。
如果是linux系统,只要有JDK环境,无需额外配置,到RocketMQ目录下能直接运行
nohup sh bin/mqnamesrv &
nohup sh bin/mqbroker -n localhost:9876 &
启动RocketMQ需要内存大一点,如linux虚拟机512MB内存,一般是启动不了的。可以修改bin/runbroker.sh
文件的JAVA_OPT参数;windows系统启动不了的话,到bin/runbroker.cmd
修改JAVA_OPT参数
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-stream-rocketmqartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-stream-binder-rocketmqartifactId>
<version>0.9.0.RELEASEversion>
dependency>
public interface CustomSource {
@Output("output1")
MessageChannel output1();
}
在启动类中或其他相关的配置类绑定通道,增加@EnableBinding
注解
@SpringBootApplication
@EnableDiscoveryClient
@EnableBinding(CustomSource.class)
public class StreamProduceApplication {
public static void main(String[] args) {
SpringApplication.run(StreamProduceApplication.class, args);
}
}
output1
输出通道及暴露Spring Cloud Stream监控端点的配置server:
port: 8081
spring:
application:
name: produce # 应用名
cloud:
nacos:
discovery:
server-addr: localhost:8848 # nacos服务地址
stream:
rocketmq:
binder:
name-server: localhost:9876 # rocketmq的服务地址
group: group
bindings:
output1:
destination: test-topic # 消息主题
content-type: application/json # 数据类型
management:
endpoints:
web:
exposure:
include: '*' # 公开所有端点, SpringCloudStream的端点: /actuator/bindings, /actuator/channels, /actuator/health
endpoint:
health:
show-details: always # 显示服务信息详情
@Service
public class SendMessageService {
@Resource
private CustomSource customSource;
public String sendMessage() {
String payload = "发送简单字符串测试" + RandomUtils.nextInt(0, 500);
customSource.output1().send(MessageBuilder.withPayload(payload).build()); // 发送消息
return payload;
}
}
@RestController
public class TestController {
@Resource
SendMessageService messageService;
@RequestMapping("/sendMessage")
public String sendMessage() {
return messageService.sendMessage();
}
}
public interface CustomSink {
@Input("input1")
SubscribableChannel input1();
}
并且在启动类中绑定
@SpringBootApplication
@EnableDiscoveryClient
@EnableBinding(CustomSink.class)
public class StreamConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(StreamConsumerApplication.class, args);
}
}
server:
port: 8082
spring:
application:
name: consumer # 应用名
cloud:
nacos:
discovery:
server-addr: localhost:8848 # nacos服务地址
stream:
rocketmq:
binder:
name-server: localhost:9876 # rocketmq的服务地址
group: group
bindings:
input1:
destination: test-topic # 消息主题
content-type: application/json # 数据类型
group: test-group # 消费者组,防止多个实例重复消费,相同组只有一个实例能收到
management:
endpoints:
web:
exposure:
include: '*' # 公开所有端点, SpringCloudStream的端点: /actuator/bindings, /actuator/channels, /actuator/health
endpoint:
health:
show-details: always # 显示服务信息详情
@Component
public class ConsumerListener {
// 接收处理消息,接收字符串
@StreamListener("input1")
public void input1Consumer(String message) {
System.out.println("input1Consumer received " + message);
}
// 接收处理消息,接收最原始的Message
@StreamListener("input1")
public void input1ConsumerMessage(Message<String> message) {
String payload = message.getPayload();
MessageHeaders headers = message.getHeaders();
System.out.println("input1Consumer Message - 消息内容 " + payload + " 消息头 " + headers );
}
}
input1Consumer()
和input1ConsumerMessage()
都可以消费信息,不同的是它们接收的数据不相同,
input1ConsumerMessage()
可以拿到更多的信息,如头信息等
将生产者和消费者启动起来,cmd测下接口
curl localhost:8081/sendMessage
发送简单字符串测试13
看到消费者日志,也拿到了消息
生产者创建一个对象类
public class User {
private Integer id;
private String name;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
SendMessageService类增加sendObjectMessage方法
public String sendObjectMessage() {
User user = new User();
user.setId(RandomUtils.nextInt(0, 500));
user.setName("ZhangSan");
customSource.output1().send(MessageBuilder.withPayload(user).build()); // 发送消息
return "用户id" + user.getId() + " 用户名:" + user.getName();
}
/sendObjectMessage
@RequestMapping("/sendObjectMessage")
public String sendObjectMessage() {
return messageService.sendObjectMessage();
}
curl localhost:8081/sendObjectMessage
用户id221 用户名:ZhangSan
日志
若出现重复消费问题,一般是以下两种情况导致:
spring.cloud.stream.bindings..group
的组不同,那么它们会没人消费一次,造成多次消费,解决:把它们放置到相同的组中即可消息过滤有两种方案
@StreamListener注解condition属性
消息生产者SendMessageService类中,增加sendConsitionMessage方法,发送自定义头Custom-header
的消息
public String sendConditionMessage() {
String payload = "发送请求头字符串测试" + RandomUtils.nextInt(0, 500);
customSource.output1().send(MessageBuilder
.withPayload(payload)
.setHeader("custom-header", "customHeader") // 设置头信息
.build()); // 发送消息
return payload;
}
@RequestMapping("/sendConditionMessage")
public String sendConditionMessage() {
return messageService.sendConditionMessage();
}
@StreamListener(value = "input1", condition = "headers['custom-header']=='customHeader'")
public void input1Consumer(String message) {
System.out.println("input1Consumer received " + message);
}
curl localhost:8081/sendConditionMessage
发送请求头字符串测试344
消息过滤–tags
生产者SendMessageService增加sendTagsMessage方法
public String sendTagsMessage() {
String payload = "发送带有tags测试" + RandomUtils.nextInt(0, 500);
customSource.output1().send(MessageBuilder
.withPayload(payload)
.setHeader(RocketMQConst.Headers.TAGS, "test") // 设置头信息
.build()); // 发送消息
return payload;
}
/sendTagsMessage
@RequestMapping("/sendTagsMessage")
public String sendTagsMessage() {
return messageService.sendTagsMessage();
}
server:
port: 8082
spring:
application:
name: consumer # 应用名
cloud:
nacos:
discovery:
server-addr: localhost:8848 # nacos服务地址
stream:
rocketmq:
binder:
name-server: localhost:9876 # rocketmq的服务地址
group: group
bindings:
input1:
consumer:
tags: test # 指定input1消费带有tags为test的消息,若多个用 || 隔开,如 test1 || test2,就是消费test1或test2
bindings:
input1:
destination: test-topic # 消息主题
content-type: application/json # 数据类型
group: test-group # 消费者组,防止多个实例重复消费,相同组只有一个实例能收到
management:
endpoints:
web:
exposure:
include: '*' # 公开所有端点, SpringCloudStream的端点: /actuator/bindings, /actuator/channels, /actuator/health
endpoint:
health:
show-details: always # 显示服务信息详情
curl localhost:8081/sendTagsMessage
发送带有tags测试323
消息统一的异常处理分为局部异常处理和针对某个主题的全局异常处理
局部
在消费者端,创建一个异常处理类
@Component
public class HandleConsumerError {
// 处理test-topic下的test-group分组中的异常
@ServiceActivator(inputChannel = "test-topic.test-group.errors")
public void handleError(ErrorMessage message) {
Throwable payload = message.getPayload();
System.out.println("截获异常:" + payload.getMessage());
System.out.println("原始消息:" + new String((byte[])
((MessagingException) payload).getFailedMessage().getPayload()));
}
}
@StreamListener("input1")
public void input1Consumer(String message) {
System.out.println("input1Consumer received " + message);
int i = 1/0;
}
从日志中看出来,消费了三次,然后捕获异常,那么也就是有2次重试的机制。重试完毕后,还报错,那么就会捕获此异常
全局如何配置呢?
@StreamListener("errorChannel")
public void errorChannel(ErrorMessage message) {
Throwable payload = message.getPayload();
System.out.println("截获异常:" + payload.getMessage());
System.out.println("原始消息:" + new String((byte[])
((MessagingException) payload).getFailedMessage().getPayload()));
}
全局的是需要监听errorChannel
的通道即可。
那么需要注意:当局部和全局都配置时,先走局部的,局部的捕获后,全局就不会再去走了
生产者中的yml配置文件中,增加事务消息的输出信道,output2
,事务订阅主题及事务分组等
server:
port: 8081
spring:
application:
name: produce # 应用名
cloud:
nacos:
discovery:
server-addr: localhost:8848 # nacos服务地址
stream:
rocketmq:
binder:
name-server: localhost:9876 # rocketmq的服务地址
group: group
bindings:
output1:
destination: test-topic # 消息主题
content-type: application/json # 数据类型
output2:
destination: transaction-topic # 消息主题
content-type: application/json # 数据类型
management:
endpoints:
web:
exposure:
include: '*' # 公开所有端点, SpringCloudStream的端点: /actuator/bindings, /actuator/channels, /actuator/health
endpoint:
health:
show-details: always # 显示服务信息详情
@Output("output2")
MessageChannel output2();
@Autowired
private RocketMQTemplate rocketMQTemplate;
public String sendTransactionalMessage() {
String uuid = UUID.randomUUID().toString();
String payload = "发送事务测试消息" + uuid;
Message<String> build = MessageBuilder
.withPayload(payload)
.setHeader(RocketMQHeaders.TRANSACTION_ID, uuid) // 设置头信息
.build();
rocketMQTemplate.sendMessageInTransaction("myTxProducerGroup", "transaction-topic", build, uuid);
return payload;
}
/sendTransactionalMessage
@RequestMapping("/sendTransactionalMessage")
public String sendTransactionalMessage() {
return messageService.sendTransactionalMessage();
}
@RocketMQTransactionListener(txProducerGroup = "myTxProducerGroup", corePoolSize = 5, maximumPoolSize = 10) // 配置文件中配置的事务分组
public class TransactionalListener implements RocketMQLocalTransactionListener {
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) { // 执行本地事务
try {
String transactionalId = (String) msg.getHeaders().get(RocketMQHeaders.TRANSACTION_ID);
System.out.println("executeLocalTransaction transactionalId:" + transactionalId +" date = " +new Date());
// 这里做业务处理逻辑
// 成功则返回 RocketMQLocalTransactionState.COMMIT
// 失败返回 RocketMQLocalTransactionState.ROLLBACK
// 提交
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
// 报错回滚
return RocketMQLocalTransactionState.ROLLBACK;
}
}
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) { // 检查本地事务
// 一定时间后,还有消息为确认发出,RocketMQ会主动调用发送方,让调用方决定消息是否该发出,该方法决定该消息是否应该提交还是回滚
String transactionalId = (String) msg.getHeaders().get(RocketMQHeaders.TRANSACTION_ID);
System.out.println("checkLocalTransaction transactionalId:" + transactionalId +" date = " +new Date());
// todo 这里可以检验数据库是否成功入库,成功返回 commit,否则rollback
// 提交
return RocketMQLocalTransactionState.COMMIT;
}
}
executeLocalTransaction()方法:发送预备消息后,首先在此方法中执行本地事务,若成功,则提交事务,否则回滚事务,具体步骤:
checkLocalTransaction()方法:若一定时间后,还有消息未确认发出,RocketMQ会主动调用发送方,最后调用checkLocalTransaction()方法,让调用方决定消息提交还是回滚,具体步骤:
在消费者yml配置中
server:
port: 8082
spring:
application:
name: consumer # 应用名
cloud:
nacos:
discovery:
server-addr: localhost:8848 # nacos服务地址
stream:
rocketmq:
binder:
name-server: localhost:9876 # rocketmq的服务地址
group: group
bindings:
input1:
consumer:
tags: test # 指定input1消费带有tags为test的消息,若多个用 || 隔开,如 test1 || test2,就是消费test1或test2
bindings:
input1:
destination: test-topic # 消息主题
content-type: application/json # 数据类型
group: test-group # 消费者组,防止多个实例重复消费,相同组只有一个实例能收到
input2:
destination: transaction-topic # 消息主题
content-type: text/plain # 数据类型
group: transaction-group # 消费者组,防止多个实例重复消费,相同组只有一个实例能收到
management:
endpoints:
web:
exposure:
include: '*' # 公开所有端点, SpringCloudStream的端点: /actuator/bindings, /actuator/channels, /actuator/health
endpoint:
health:
show-details: always # 显示服务信息详情
@Input("input2")
SubscribableChannel input2();
receiveTransactionalMsg
@StreamListener("input2")
public void receiveTransactionalMsg(String message) {
try {
System.out.println("receiveTransactionalMsg received " + message);
} catch (Exception e) {
// 处理报错时,可以记录消息数据,采用人工干预手段,达到数据一致行
// 或者要回滚上游事务时,将此条消息数据反馈给上游,上游删除此条数据
}
}
curl localhost:8081/sendTransactionalMessage
发送事务测试消息67d2785d-9f07-4489-84ad-516bd1764633
生产者日志
当执行本地事务时,如果发生异常回滚后,消费者是接收不到这条消息的,且上游(生产者)自行回滚自己的数据就可以了。