目录
背景
上一篇博文Spring cloud Stream 入门 讲述了spring cloud stream 的背景,组成,以及基于rocketMq 搭建简单的demo 。完成消息的发送和消费。但是在实际工作过程中,我们要用到的远远不局限于简单的demo .
比如自定义接口、事务消息、消息的筛选,异常处理,消息的分区等;下面一一来列举出这些基于spring cloud stream 该如何实现呢?
自定义接口
在上一篇博文demo中,我们发送消息使用的是Source.class 中的output 方法,在配置文件中为output 指定了destination,因为一个channel 接口只能绑定一个destination ,所以在业务系统各种场景中,一个output 往往不能满足我们对消息的发送需求。这时候我们可以根据自定义接口,实现指定outputChannel ;
生产者
定义接口
/**
* @Author corn
* @Date 2021/3/21 15:44
*/
public interface OutPutSource {
String MY_CUSTOM_OUTPUT = "my-custom-output";
@Output(MY_CUSTOM_OUTPUT)
MessageChannel customOutPut();
}
加注解
在Application 启动类的@EnableBinding({OutputSource.class,Source.class}),目的通过spring ioc ,创建出对应的实例;
加配置
spring:
cloud:
stream:
rocketmq:
binder:
name-server: localhost:9876
bindings:
output:
# 用例指定topic
destination: stream-test-topic
my-custom-output:
destination: custom-output-send
发消息
/**
* @Author corn
* @Date 2021/3/21 15:47
*/
@Slf4j
@RequiredArgsConstructor(onConstructor = @_(@Autowired))
@RestController("/testStream")
public class StreamMqTest {
private final OutPutSource outPutSource;
/**
*@描述 实现自定义接口发送消息
*@参数
*@返回值
*@创建人 corn
*@创建时间 2021/3/21
*/
@GetMapping("/customOutput")
public Boolean SendCustomMessage(){
boolean result = this.outPutSource.customOutPut().send(MessageBuilder.withPayload("测试发送自定义接口消息").build());
return result;
}
}
消费者
定义接口
/**
* @Author corn
* @Date 2021/3/21 15:52
*/
public interface InputSink {
String CUSTOM_INPUT = "custom-input";
@Input(CUSTOM_INPUT)
SubscribableChannel customInput();
}
改注解
在Application 启动类的@EnableBinding({InputSink.class,Sink.class}),目的通过spring ioc ,创建出对应的实例;
加配置
spring:
cloud:
stream:
rocketmq:
binder:
name-server: 127.0.0.1:9876
bindings:
input:
destination: stream-test-topic
# 如果使用的是RocketMq,group 一定要填写,如果使用的是非RocketMq ,group 可以不填写
group: binder-group
custom-input:
destination: custom-output-send
group: custom-group1
消费消息
/**
* @Author corn
* @Date 2021/3/21 15:53
*/
@Service
@Slf4j
public class StreamMqTest {
/**
*@描述 自定义接口消息消费
*@参数
*@返回值
*@创建人 corn
*@创建时间 2021/3/21
*/
@StreamListener(InputSink.CUSTOM_INPUT)
public void messageConsumer(String messageBody){
log.info("获取消息消费内容为:{}",messageBody);
}
}
消息筛选(Tag、自定义消息筛选)
RocketMq 支持消息按照tag ,或者sql92 的方式进行筛选消费,下面我们就基于spring cloud stream 实现rocket mq 的消息筛选功能。我们知道rocketMq 消息的筛选是在consumer 实现的,所以对于消息生产者么有什么改变,重点在于consumer 端的修改;
生产者
加接口
/**
* @Author corn
* @Date 2021/3/21 15:44
*/
public interface OutPutSource {
String MY_CUSTOM_OUTPUT_TAG = "my-custom-output-tag";
@Output(MY_CUSTOM_OUTPUT_TAG)
MessageChannel customOutPutTag();
}
加配置
spring:
cloud:
stream:
rocketmq:
binder:
name-server: localhost:9876
bindings:
my-custom-output-tag:
destination: custom-output-tag
发送消息
/**
* @Author corn
* @Date 2021/3/21 15:47
*/
@Slf4j
@RequiredArgsConstructor(onConstructor = @_(@Autowired))
@RestController("/testStream")
public class StreamMqTest {
private final OutPutSource outPutSource;
/**
*@描述 自定义发送header 消息头消息。注意: header 中的内容在传输过程中都会被转换为String 字符串;
* 及如果想在header 中携带对象,需要先将对象序列化成json 串,然后传输;
*@参数
*@返回值
*@创建人 corn
*@创建时间 2021/3/21
*/
@GetMapping("/customOutPutTag")
public Boolean SendCustomTagMessage(){
// 分别发送tag 为tag1 和tag2 的消息,并且tag1 消息中指定version 分别为1 和2 。用来做消费端消息的自定义筛选
this.outPutSource.customOutPutTag().send(MessageBuilder.withPayload("发送带有header 的消息体,供消费端tag1筛选")
.setHeader(RocketMQHeaders.TAGS,"tag1")
.setHeader("version","1")
.build());
this.outPutSource.customOutPutTag().send(MessageBuilder.withPayload("发送带有header 的消息体,供消费端tag1筛选")
.setHeader(RocketMQHeaders.TAGS,"tag1")
.setHeader("version","2")
.build());
this.outPutSource.customOutPutTag().send(MessageBuilder.withPayload("发送带有header 的消息体,供消费者tag2筛选")
.setHeader(RocketMQHeaders.TAGS,"tag2")
.build());
return true;
}
}
消费者
加接口
/**
* @Author corn
* @Date 2021/3/21 15:52
*/
public interface InputSink {
String CUSTOM_INPUT_TAG = "custom-input-tag";
@Input(CUSTOM_INPUT_TAG)
SubscribableChannel customInputTag();
}
加配置
spring:
cloud:
stream:
rocketmq:
binder:
name-server: 127.0.0.1:9876
# 指定bindings 消费者匹配规则
bindings:
custom-input-tag:
consumer:
# 表示筛选custom-input-tag channel 下tag1 的消息,多个可以用||来表示,比如tags: tag1||tag2 表示根据tag1 或者tag2 进行消费
tags: tag1
bindings:
custom-input-tag:
destination: custom-output-send-tag
group: custom-group2
消费消息
/**
*@描述 condition 用作筛选,该方法表示只消费InputSink.custom_input_tag下 headers 中version == 1 的消息
*@参数
*@返回值
*@创建人 corn
*@创建时间 2021/3/21
*/
@StreamListener(value = InputSink.CUSTOM_INPUT_TAG,condition = "headers['version']=='1'")
public void messageConsumer1(String messageBody){
log.info("获取tag 和自定义header 筛选后的消息内容为:{}",messageBody);
}
输出结果
消息筛选(sql92 方式)
注意:sql92 是rocketMq 特有的消息筛选方式,sql92 支持> 、<、=、or、and 等方式;
Sql92 方式的消息筛选,可以更加灵活的对消息内容进行筛选,比如通过sql92 的方式筛选某个区间的消息等等;sql92 支持使用
生产者
加接口
/**
* @Author corn
* @Date 2021/3/21 15:44
*/
public interface OutPutSource {
String MY_CUSTOM_SQL_92 = "my-custom-sql92";
@Output(MY_CUSTOM_SQL_92)
MessageChannel customSql();
}
加配置
spring:
cloud:
stream:
rocketmq:
binder:
name-server: localhost:9876
bindings:
my-custom-sql92:
destination: custom-output-sql
发送消息
/**
*@描述 发送消息
*@参数
*@返回值
*@创建人 zj
*@创建时间 2021/3/21
*/
@GetMapping("/sendSql")
public Boolean sendsql92Message(){
this.outPutSource.customSql().send(
MessageBuilder.withPayload("携带sql92的消息体1")
.setHeader("num",1)
.build()
);
this.outPutSource.customSql().send(
MessageBuilder.withPayload("携带sql92的消息体2")
.setHeader("num",2)
.build()
);
this.outPutSource.customSql().send(
MessageBuilder.withPayload("携带sql92的消息体3")
.setHeader("num",3)
.build()
);
return true;
}
消费者
加接口
/**
* @Author corn
* @Date 2021/3/21 15:52
*/
public interface InputSink {
String CUSTOM_INPUT_SQL = "custom-input-sql";
@Input(CUSTOM_INPUT_SQL)
SubscribableChannel customInputSql();
}
加配置
spring:
cloud:
stream:
rocketmq:
binder:
name-server: 127.0.0.1:9876
# 指定bindings 消费者匹配规则
bindings:
custom-input-sql:
consumer:
# 表示筛选custom-input-sql channel 下的header 中携带的num 大于2 的消息
sql: 'num>2'
bindings:
custom-input-sql:
destination: custom-output-sql
group: custom-group3
消费消息
/**
*@描述 根据sql92 方式筛选消息
*@参数
*@返回值
*@创建人 corn
*@创建时间 2021/3/21
*/
@StreamListener(InputSink.CUSTOM_INPUT_SQL)
public void MessageConsumer2(String messageBody){
log.info("获取sql92 筛选后的消息内容为:{}",messageBody);
}
输出结果
事务消息发送
rocketMq 完美支持了事务消息,但是当我们使用springcloud stream 时,是如何支持事务消息的呢?
事务消息的发送,主要体现在produce 端 对消息的处理;
生产者
加接口
/**
* @Author corn
* @Date 2021/3/21 15:44
*/
public interface OutPutSource {
String MY_CUSTOM_TRANSACTION = "my-custom-transaction";
@Output(MY_CUSTOM_TRANSACTION)
MessageChannel customTransaction();
}
加配置
spring:
cloud:
stream:
rocketmq:
binder:
name-server: localhost:9876
bindings:
# 对my-custom-transaction channel 开启事务消息
my-custom-transaction:
producer:
transactional: true
# 注意,这里的group 要和事务listener 中的txProducerGroup 值保持一致,否则listener 监听到事务消息
group: tx-test-transcation-message
bindings:
my-custom-transaction:
destination: custom-transaction
发送消息
/**
*@描述 发送事务消息
*@参数
*@返回值
*@创建人 corn
*@创建时间 2021/3/21
*/
@GetMapping("/sendTranscationMessage")
public boolean sendTranscationMessage(){
this.outPutSource.customTransaction().send(
// 消息内容,setHeader 主要传递参数,供事务回调使用
MessageBuilder.withPayload("事务消息对象")
.setHeader(RocketMQHeaders.TRANSACTION_ID,1)
.setHeader("share_id",3)
.setHeader("user", "需要携带对象的json串")
.build()
);
return true;
}
事务消息监听类
package com.springcloud.alibaba.controller;
import com.alibaba.fastjson.JSONObject;
import com.springcloud.alibaba.model.CanalUser;
import com.springcloud.alibaba.rocketmq.OutPutSource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;
import org.apache.rocketmq.spring.support.RocketMQHeaders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
/**
* @Author corn
* @Date 2021/3/21 18:31
*/
@RocketMQTransactionListener(txProducerGroup = "tx-test-transcation-message")
@RequiredArgsConstructor(onConstructor = @_(@Autowired))
@Slf4j
public class TranscationListener implements RocketMQLocalTransactionListener {
/**
*@描述 执行本地事务
*@参数
*@返回值
*@创建人 corn
*@创建时间 2021/3/21
*/
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {
MessageHeaders headers = message.getHeaders();
String transactionId = (String)headers.get(RocketMQHeaders.TRANSACTION_ID);
try {
log.info("事务消息内容为:{}",message);
log.info("获取到事务的transactionId 为{}",transactionId);
// 调用本地方法事务逻辑,如果执行成功,则提交事务,反之回滚事务
Thread.sleep(500);
// 提交事务
return RocketMQLocalTransactionState.COMMIT;
}catch (Exception e){
log.error("异常:{}",e.getMessage());
// 事务回滚
return RocketMQLocalTransactionState.ROLLBACK;
}
}
/**
*@描述 事务回调方法,发生在生产者没有告诉broker事务消息的执行状态,broker会调用回查方法查询本地事务执行状态;
* 需要注意的是,broker 并非无休止的轮查,默认回查15次,如果还未查到消息的状态,那么就会回滚该条事务消息;
* 注意: RocketMq 3.1.2之前的有事务消息回查功能的版本。消息回查功能基于文件系统,回查后得到的结果以及正常的处理结果Commit/Rollback都会修改CommitLog里PREPARED消息的状态
* ,这会导致内存中脏页过多,有隐患。在之后的版本移除了基于文件系统的状态修改机制,对事务消息的处理流程进行重做,移除了消息回查功能。
*@参数
*@返回值
*@创建人 corn
*@创建时间 2021/3/16
*/
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
return null;
}
}
分区消息
在一些业务场景下,集群环境下,我们希望同一类型的消息,在消费组中能够在同一个实例内进行处理。比如同一个订单号在同一个实例内进行处理,这样可以在一定程度上保证消息的有序消费;下面来具体实现消息的分区处理;
生产者
加接口
/**
* @Author corn
* @Date 2021/3/21 15:44
*/
public interface OutPutSource {
String MY_CUSTOM_PARTITION = "my-custom-partition";
@Output(MY_CUSTOM_PARTITION)
MessageChannel customPartition();
}
加配置
spring:
cloud:
stream:
rocketmq:
binder:
name-server: localhost:9876
bindings:
my-custom-partition:
destination: custom-partition
producer:
# 根据什么字段进行分区,payload 是消息体中的对象。这样就能够保证同一个对象落到同一个实例 中。当然我们也可以根据payload 中的属性进行分区,比如根据订单号payload.orderId。前提需要保证消息体汇总含有orderId 属性字段
partitionKeyExpression: payload
# 分区个数,这里分了2个区
partitionCount: 2
发送消息
/**
*@描述 发送分区消息
*@参数
*@返回值
*@创建人 corn
*@创建时间 2021/3/21
*/
@GetMapping("/sendPartitionMessage")
public boolean sendPartitionMessage(){
CanalUser build = CanalUser.builder().name("张三").sex("男").age(2).build();
boolean result = this.outPutSource.customPartition().send(MessageBuilder.withPayload(build).build());
CanalUser build1 = CanalUser.builder().name("张三").sex("男").age(1).build();
boolean result1 = this.outPutSource.customPartition().send(MessageBuilder.withPayload(build1).build());
log.info("消息发送:{}",result);
return true;
}
消费者
加接口
/**
* @Author corn
* @Date 2021/3/21 15:52
*/
public interface InputSink {
String CUSTOM_INPUT_PARTITION = "custom-input-partition";
@Input(CUSTOM_INPUT_PARTITION)
SubscribableChannel customInputPartition();
}
加配置
spring:
cloud:
stream:
rocketmq:
binder:
name-server: 127.0.0.1:9876
bindings:
custom-input-partition:
destination: custom-partition
group: custom-group4
# 表示分区的个数
instance-count: 2
# 表示当前的分区是什么,从0 开始,设置的当前分区的最大分区不得超过分区个数,否则设置当前分区无效
instance-index: 1
消费消息
/**
*@描述 分区消息消费
*@参数
*@返回值
*@创建人 corn
*@创建时间 2021/3/21
*/
@StreamListener(InputSink.CUSTOM_INPUT_PARTITION)
public void MessageConsumer3(String messageBody){
log.info("获取分区消息消费内容:{}",messageBody);
}
异常处理
Spring Cloud Stream 异常处理有4类,分别是
自动重试
自定义错误处理逻辑
加入死信队列
从新加入队列消费
这里优先实现消息全局自定义逻辑处理,更多的异常处理方式可以参考大目老师的博文spring cloud stream 错误详解
全局异常处理编码
/**
*@描述 全局消息消费异常处理
*@参数
*@返回值
*@创建人 corn
*@创建时间 2021/3/19
*/
@StreamListener("errorChannel")
public void error(Message<?> message) {
ErrorMessage errorMessage = (ErrorMessage) message;
log.error("消息消费失败啦,异常消息内容为:{}",errorMessage);
}
总结
基于上一篇简单的实现spring cloud stream 基础后,本文详细从编码层实现了sprign cloud stream 的其他属性。在日常的工作中,这些属性内容足以完成需求实现,更细致的在后期工作中遇到在进行记录;