事务消息流程分析
上图说明了事务消息的大致方案,其中分为两个流程:正常事务消息的发送及提交、事务消息的补偿流程。
1.事务消息发送及提交:
(1) 发送消息(half消息)。
(2) 服务端(Broker)响应消息写入结果。
(3) 根据发送结果执行本地事务(如果写入失败,此时half消息对业务不可见,本地逻辑不执行)。
(4) 根据本地事务状态执行Commit或者Rollback(Commit操作生成消息索引,消息对消费者可见)
2.补偿流程:
(1) 对没有Commit/Rollback的事务消息(pending状态的消息),从服务端发起一次“回查”
(2) Producer收到回查消息,检查回查消息对应的本地事务的状态
(3) 根据本地事务状态,重新Commit或者Rollback
其中,补偿阶段用于解决消息Commit或者Rollback发生超时或者失败的情况。
事务消息共有三种状态,提交状态、回滚状态、中间状态:
- TransactionStatus.CommitTransaction: 提交事务,它允许消费者消费此消息。
- TransactionStatus.RollbackTransaction: 回滚事务,它代表该消息将被删除,不允许被消费。
- TransactionStatus.Unknown: 中间状态,它代表需要检查消息队列来确定状态。
样例代码
发送
生产者
private void testRocketMQTemplateTransaction() throws MessagingException {
String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
for (int i = 0; i < 10; i++) {
try {
Message msg = MessageBuilder.withPayload("rocketMQTemplate transactional message " + i + tags[i % tags.length]).
setHeader(RocketMQHeaders.TRANSACTION_ID, "KEY_" + i).build();
SendResult sendResult = rocketMQTemplate.sendMessageInTransaction(
springTransTopic + ":" + tags[i % tags.length], msg, null);
System.out.printf("------rocketMQTemplate send Transactional msg body = %s , sendResult=%s %n",
msg.getPayload(), sendResult.getSendStatus());
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
}
}
事务消息监听器
- 监听器包含两个接口方法,执行事务回调的方法 executeLocalTransaction 和 事务返回 UNKNOWN 的回查方法 RocketMQLocalTransactionState
- TransactionListenerImpl 实现 RocketMQLocalTransactionListener 监听器接口,重写 executeLocalTransaction 方法 和 RocketMQLocalTransactionState 方法
- public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg)
执行事务方法,发送事务消息后,会回调该方法_- public RocketMQLocalTransactionState checkLocalTransaction(Message msg)
事务回查方法,如果事务消息status = UNKNOWN,会不断回调该方法,默认最多回查15次_
@RocketMQTransactionListener
class TransactionListenerImpl implements RocketMQLocalTransactionListener {
private AtomicInteger transactionIndex = new AtomicInteger(0);
//保存所有事务消息id -> status的映射
private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<String, Integer>();
//执行事务方法,发送事务消息后,会回调该方法
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
String transId = (String) msg.getHeaders().get(RocketMQHeaders.TRANSACTION_ID);
System.out.printf("#### executeLocalTransaction is executed, msgTransactionId=%s %n",
transId);
int value = transactionIndex.getAndIncrement();
int status = value % 3;
localTrans.put(transId, status);
//COMMIT 本地事务执行成功,提交
if (status == 0) {
// Return local transaction with success(commit), in this case,
// this message will not be checked in checkLocalTransaction()
System.out.printf(" # COMMIT # Simulating msg %s related local transaction exec succeeded! ### %n", msg.getPayload());
return RocketMQLocalTransactionState.COMMIT;
}
//ROLLBACK 本地事务执行失败,回滚,删除消息
if (status == 1) {
// Return local transaction with failure(rollback) , in this case,
// this message will not be checked in checkLocalTransaction()
System.out.printf(" # ROLLBACK # Simulating %s related local transaction exec failed! %n", msg.getPayload());
return RocketMQLocalTransactionState.ROLLBACK;
}
//UNKNOWN 本地事务执行状态未知,执行回查方法 checkLocalTransaction
System.out.printf(" # UNKNOW # Simulating %s related local transaction exec UNKNOWN! \n");
return RocketMQLocalTransactionState.UNKNOWN;
}
//事务回查方法,如果事务消息status = UNKNOWN,会不断回调该方法,默认最多回查15次
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
String transId = (String) msg.getHeaders().get(RocketMQHeaders.TRANSACTION_ID);
RocketMQLocalTransactionState retState = RocketMQLocalTransactionState.COMMIT;
Integer status = localTrans.get(transId);
if (null != status) {
switch (status) {
case 0:
retState = RocketMQLocalTransactionState.COMMIT;
break;
case 1:
retState = RocketMQLocalTransactionState.ROLLBACK;
break;
case 2:
retState = RocketMQLocalTransactionState.UNKNOWN;
break;
}
}
System.out.printf("------ !!! checkLocalTransaction is executed once," +
" msgTransactionId=%s, TransactionState=%s status=%s %n",
transId, retState, status);
return retState;
}
}
接收
消费者
标签过滤 selectorExpression = “TagA || TagC”
@Service
@RocketMQMessageListener(topic = "${demo.rocketmq.transTopic}", consumerGroup = "string_trans_consumer",selectorExpression = "TagA || TagC")
public class StringTransactionalConsumer implements RocketMQListener<String> {
@Override
public void onMessage(String message) {
System.out.printf("------- StringTransactionalConsumer received: %s \n", message);
}
}
执行结果
生产者
#### executeLocalTransaction is executed, msgTransactionId=KEY_0
# COMMIT # Simulating msg [B@6215366a related local transaction exec succeeded! ###
------rocketMQTemplate send Transactional msg body = rocketMQTemplate transactional message 0TagA , sendResult=SEND_OK
#### executeLocalTransaction is executed, msgTransactionId=KEY_1
# ROLLBACK # Simulating [B@3f96f020 related local transaction exec failed!
------rocketMQTemplate send Transactional msg body = rocketMQTemplate transactional message 1TagB , sendResult=SEND_OK
#### executeLocalTransaction is executed, msgTransactionId=KEY_2
# UNKNOW # Simulating ------rocketMQTemplate send Transactional msg body = rocketMQTemplate transactional message 2TagC , sendResult=SEND_OK
#### executeLocalTransaction is executed, msgTransactionId=KEY_3
# COMMIT # Simulating msg [B@1f2f0109 related local transaction exec succeeded! ###
------rocketMQTemplate send Transactional msg body = rocketMQTemplate transactional message 3TagD , sendResult=SEND_OK
#### executeLocalTransaction is executed, msgTransactionId=KEY_4
# ROLLBACK # Simulating [B@1faf386c related local transaction exec failed!
------rocketMQTemplate send Transactional msg body = rocketMQTemplate transactional message 4TagE , sendResult=SEND_OK
#### executeLocalTransaction is executed, msgTransactionId=KEY_5
# UNKNOW # Simulating ------rocketMQTemplate send Transactional msg body = rocketMQTemplate transactional message 5TagA , sendResult=SEND_OK
#### executeLocalTransaction is executed, msgTransactionId=KEY_6
# COMMIT # Simulating msg [B@771a7d53 related local transaction exec succeeded! ###
------rocketMQTemplate send Transactional msg body = rocketMQTemplate transactional message 6TagB , sendResult=SEND_OK
#### executeLocalTransaction is executed, msgTransactionId=KEY_7
# ROLLBACK # Simulating [B@5a917723 related local transaction exec failed!
------rocketMQTemplate send Transactional msg body = rocketMQTemplate transactional message 7TagC , sendResult=SEND_OK
#### executeLocalTransaction is executed, msgTransactionId=KEY_8
# UNKNOW # Simulating ------rocketMQTemplate send Transactional msg body = rocketMQTemplate transactional message 8TagD , sendResult=SEND_OK
#### executeLocalTransaction is executed, msgTransactionId=KEY_9
# COMMIT # Simulating msg [B@59b32539 related local transaction exec succeeded! ###
------rocketMQTemplate send Transactional msg body = rocketMQTemplate transactional message 9TagE , sendResult=SEND_OK
------ !!! checkLocalTransaction is executed once, msgTransactionId=KEY_5, TransactionState=UNKNOWN status=2
------ !!! checkLocalTransaction is executed once, msgTransactionId=KEY_8, TransactionState=UNKNOWN status=2
------ !!! checkLocalTransaction is executed once, msgTransactionId=KEY_5, TransactionState=UNKNOWN status=2
------ !!! checkLocalTransaction is executed once, msgTransactionId=KEY_2, TransactionState=UNKNOWN status=2
------ !!! checkLocalTransaction is executed once, msgTransactionId=KEY_2, TransactionState=UNKNOWN status=2
------ !!! checkLocalTransaction is executed once, msgTransactionId=KEY_5, TransactionState=UNKNOWN status=2
------ !!! checkLocalTransaction is executed once, msgTransactionId=KEY_8, TransactionState=UNKNOWN status=2
------ !!! checkLocalTransaction is executed once, msgTransactionId=KEY_8, TransactionState=UNKNOWN status=2
------ !!! checkLocalTransaction is executed once, msgTransactionId=KEY_2, TransactionState=UNKNOWN status=2
------ !!! checkLocalTransaction is executed once, msgTransactionId=KEY_2, TransactionState=UNKNOWN status=2
------ !!! checkLocalTransaction is executed once, msgTransactionId=KEY_5, TransactionState=UNKNOWN status=2
------ !!! checkLocalTransaction is executed once, msgTransactionId=KEY_2, TransactionState=UNKNOWN status=2
------ !!! checkLocalTransaction is executed once, msgTransactionId=KEY_5, TransactionState=UNKNOWN status=2
------ !!! checkLocalTransaction is executed once, msgTransactionId=KEY_5, TransactionState=UNKNOWN status=2
------ !!! checkLocalTransaction is executed once, msgTransactionId=KEY_8, TransactionState=UNKNOWN status=2
------ !!! checkLocalTransaction is executed once, msgTransactionId=KEY_8, TransactionState=UNKNOWN status=2
------ !!! checkLocalTransaction is executed once, msgTransactionId=KEY_8, TransactionState=UNKNOWN status=2
------ !!! checkLocalTransaction is executed once, msgTransactionId=KEY_2, TransactionState=UNKNOWN status=2
------ !!! checkLocalTransaction is executed once, msgTransactionId=KEY_8, TransactionState=UNKNOWN status=2
------ !!! checkLocalTransaction is executed once, msgTransactionId=KEY_5, TransactionState=UNKNOWN status=2
------ !!! checkLocalTransaction is executed once, msgTransactionId=KEY_5, TransactionState=UNKNOWN status=2
------ !!! checkLocalTransaction is executed once, msgTransactionId=KEY_2, TransactionState=UNKNOWN status=2
...
...
...
消费者
------- StringTransactionalConsumer received: rocketMQTemplate transactional message 0TagA
输出分析
- 生产者的事务监听器里的 executeLocalTransaction 事务执行方法
由程序可知,对于 index = 0…9 的消息,
index % 3 = 0 , COMMIT
index % 3 = 1 , ROLLBACK
index % 3 = 2 , UNKNOW
所以会按 COMMIT、ROLLBACK、UNKNOW的顺序,循环输出
- 生产者的事务监听器里的 checkLocalTransaction 回查方法
对于 index % 3 = 2 的事务消息 ,
在 executeLocalTransaction 方法 里,返回 UNKNOW 状态
故会调用回查方法 checkLocalTransaction 方法,而且在 checkLocalTransaction 方法里,index % 3 = 2,也是返回 UNKNOW 状态,
故 index % 3 = 2 的事务消息 会不断回查,直到达到最大回查次数 15 次
故 msgTransactionId=KEY_2 、KEY_5、KEY_8 的事务消息会不断回查,不断输出回查信息
即一直进行如下循环15次
- 消费者
消费者能够接收到的消息,满足条件:
- index % 3 = 0 , COMMIT
- selectorExpression = "TagA || TagC"
index = 0…9 的10条消息里, 只有第1条消息 index = 0 的消息满足 两个条件,故消费者只输出一条消息
RocketMq-console 分析
- index = 9、index =6、index = 3
对于此类消息,即虽然满足 COMMIT 条件 index % 3 =0,但是不满足tag 过滤条件,故在消费者端,被过滤
RocketMQ分布式消息队列的消息过滤方式有别于其它MQ中间件,是在Consumer端订阅消息时再做消息过滤的。RocketMQ这么做是在于其Producer端写入消息和Consumer端订阅消息采用分离存储的机制来实现的,
—官方文档
2.tag = "TagC"
当index = 2、7 时,tag = "TagC "
- index = 2, 2 % 3 =2, 返回 UNKNOW
- index =7 , 7 % 3 =1,返回 ROLLBACK
因此,在topic =** spring-transaction-topic **找不到 tag = "TagC"的消息
只有在 topic = **RMQ_SYS_TRANS_HALF_TOPIC **里,才出现 tag = "TagC"的消息
即第一阶段的消息,对消费者不可见
在RocketMQ事务消息的主要流程中,一阶段的消息如何对用户不可见。其中,事务消息相对普通消息最大的特点就是一阶段发送的消息对用户是不可见的。那么,如何做到写入消息但是对用户不可见呢?RocketMQ事务消息的做法是:如果消息是half消息,将备份原消息的主题与消息消费队列,然后改变主题为RMQ_SYS_TRANS_HALF_TOPIC。由于消费组未订阅该主题,故消费端无法消费half类型的消息,然后RocketMQ会开启一个定时任务,从Topic为RMQ_SYS_TRANS_HALF_TOPIC中拉取消息进行消费,根据生产者组获取一个服务提供者发送回查事务状态请求,根据事务状态来决定是提交或回滚消息。
事务消息的使用限制
-
事务消息不支持延时消息和批量消息。
-
为了避免单个消息被检查太多次而导致半队列消息累积,我们默认将单个消息的检查次数限制为 15 次,但是用户可以通过 Broker 配置文件的
transactionCheckMax
参数来修改此限制。如果已经检查某条消息超过 N 次的话( N =transactionCheckMax
) 则 Broker 将丢弃此消息,并在默认情况下同时打印错误日志。用户可以通过重写AbstractTransactionCheckListener
类来修改这个行为。
-
事务消息将在 Broker 配置文件中的参数 transactionMsgTimeout 这样的特定时间长度之后被检查。当发送事务消息时,用户还可以通过设置用户属性 CHECK_IMMUNITY_TIME_IN_SECONDS 来改变这个限制,该参数优先于
transactionMsgTimeout
参数。
-
事务性消息可能不止一次被检查或消费。
-
提交给用户的目标主题消息可能会失败,目前这依日志的记录而定。它的高可用性通过 RocketMQ 本身的高可用性机制来保证,如果希望确保事务消息不丢失、并且事务完整性得到保证,建议使用同步的双重写入机制。
-
事务消息的生产者 ID 不能与其他类型消息的生产者 ID 共享。与其他类型的消息不同,事务消息允许反向查询、MQ服务器能通过它们的生产者 ID 查询到消费者。