RocketMQ事务消息概述&样例
为什么要有事务消息?本地事务不能解决问题吗? 一个简单的例子: 写数据库+给MQ发送一条消息
scss
transaction {
updateDB()
success = sendMessage()
if success {
commit()
}
rollback()
}
这个过程看起来没有任何问题,但是如果sendMessage后因为网络或者其他原因导致生产者没有收到sendResult,但是消息实际已经写入了MQ,这样回滚后的结果实际是写MQ成功,写DB失败,并不符合我们的预期,所以我们希望写DB和写MQ整体具有原子性,于是就有了事务消息
事务消息的使用
java
public class TranProd {
public static void main(String[] args) throws MQClientException, InterruptedException {
// 生成一个事务消息生产者
TransactionMQProducer producer = new TransactionMQProducer("trance_produce_group");
producer.setNamesrvAddr("127.0.0.1:9876");
producer.setTransactionListener(new TranListener());
producer.start();
Message message = new Message("tranTest", "hellow".getBytes());
TransactionSendResult transactionSendResult = producer.sendMessageInTransaction(message, null);
System.out.printf("%s\n", transactionSendResult);
Thread.sleep(50000);
producer.shutdown();
}
}
// 事务Listener
public class TranListener implements TransactionListener {
public TranListener() {
}
@Override
public LocalTransactionState executeLocalTransaction(Message message, Object o) {
// 需要执行的内容
System.out.println("executeLocalTransaction");
updateDB();
return LocalTransactionState.UNKNOW;
}
@Override
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
// 检查并提交事务消息
System.out.println("checkLocalTransaction");
checkDB();
return LocalTransactionState.COMMIT_MESSAGE;
}
}
LocalTransactionState.UNKNOW: 未知的事务状态
LocalTransactionState.COMMIT_MESSAGE: 提交事务
LocalTransactionState.ROLLBACK_MESSAGE: 回滚事务
executeLocalTransaction
: 用来执行主要的逻辑,将需要看作一个整体的操作放在里面,这里我return的事务状态是UNKNOW
,其实这里就可以根据是否抛出异常来判断是不是commit/rollback事务 checkLocalTransaction
: 用来检查事务的状态,可以在这个方法中利用select来检查DB是否写入成功了,在这个方法内也可以进行commit/rollback
一些约定:
代码段中...
表示一些不重要的逻辑部分; 没有使用//
注释的汉字是真实执行的代码逻辑
RocketMQ事务消息Client端---sendMessageInTransaction部分
首先我们从生产者来看事务的生产者代码
java
public TransactionSendResult sendMessageInTransaction(final Message msg,
final TransactionListener localTransactionListener, final Object arg)
throws MQClientException {
...
检查listener
是否是延迟消息,如果是就清理延迟标记
// 打上事务消息标记
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true");
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_PRODUCER_GROUP, this.defaultMQProducer.getProducerGroup());
// 发送消息
sendResult = this.send(msg);
-------- 这里就要转到broker端了,下文的broker端代码对应的是生产者执行到这里
switch (sendResult.getSendStatus()) {
case SEND_OK: {
...
检查事务ID
// 执行本地事务
// 这里就是执行上文中样例里的业务逻辑(写DB)
transactionListener.executeLocalTransaction(msg, arg);
}
break;
case FLUSH_DISK_TIMEOUT:
case FLUSH_SLAVE_TIMEOUT:
case SLAVE_NOT_AVAILABLE:
localTransactionState = LocalTransactionState.ROLLBACK_MESSAGE;
break;
default:
break;
}
// 通知broker端根据localTransactionState进行对应的操作,这个方法我们下面在看
this.endTransaction(msg, sendResult, localTransactionState, localException);
...
拼接result
}
从上面的这段代码我们可以看到发送事务消息生产者一共做了:
- 给消息打上事务标记
- 发送消息到broker
- 如果消息发送成功就执行本地事务方法executeLocalTransaction,设置LocalTransactionState
- 执行endTransaction方法(下文中具体看这个方法的用途)
Broker端---处理sendMessage
这里我们看下生产者发送了事务消息到broker端,broker如何处理事务消息
java
public RemotingCommand processRequest(ChannelHandlerContext ctx,
RemotingCommand request) throws RemotingCommandException {
...
response = this.sendMessage(ctx, request, sendMessageContext, requestHeader, mappingContext,
(ctx12, response12) -> executeSendMessageHookAfter(response12, ctx12));
}
// sendMessage方法
public RemotingCommand sendMessage(final ChannelHandlerContext ctx,
final RemotingCommand request,
final SendMessageContext sendMessageContext,
final SendMessageRequestHeader requestHeader,
final TopicQueueMappingContext mappingContext,
final SendMessageCallback sendMessageCallback) throws RemotingCommandException {
...
// 根据PROPERTY_TRANSACTION_PREPARED这个标记来判断是不是事务消息
String traFlag = oriProps.get(MessageConst.PROPERTY_TRANSACTION_PREPARED);
...
putMessageResult = this.brokerController.getTransactionalMessageService().prepareMessage(msgInner);
...
}
// 跟着prepareMessage方法继续往下走最终会走到parseHalfMessageInner方法
private MessageExtBrokerInner parseHalfMessageInner(MessageExtBrokerInner msgInner) {
...
设置PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX
// 把真实的topic信息换存在消息中
MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_TOPIC, msgInner.getTopic());
MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_QUEUE_ID,
String.valueOf(msgInner.getQueueId()));
...
set了一个flag TRANSACTION_NOT_TYPE
// 这里将topic设置成了RMQ_SYS_TRANS_HALF_TOPIC
// 也就是说所有的事务消息最终都先写入到了RMQ_SYS_TRANS_HALF_TOPIC这个topic中
msgInner.setTopic(TransactionalMessageUtil.buildHalfTopic());
msgInner.setQueueId(0);
msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties()));
return msgInner;
}
通过上面的broker端代码我们可以知道,broker在处理sendMessage的时候做了:
- 将真实的topic信息缓存在了消息中
- 将消息写入了
RMQ_SYS_TRANS_HALF_TOPIC
这个topic中,并且落盘
RMQ_SYS_TRANS_HALF_TOPIC
是一个单队列topic,所有的事务消息都会先写入这个topic然后根据事务的状态来进行commit/rollback
Client端----endTransaction方法
这个时候生产者已经收到了sendResult,执行了listener中的executeLocalTransaction方法后得到了一个LocalTransactionState,这个时候调用endTransaction方法看看是做什么
java
public void endTransaction(...) {
switch (localTransactionState) {
case COMMIT_MESSAGE:
requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_COMMIT_TYPE);
break;
case ROLLBACK_MESSAGE:
requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_ROLLBACK_TYPE);
break;
case UNKNOW:
requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_NOT_TYPE);
break;
default:
break;
}
// RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.END_TRANSACTION, requestHeader);
// 上面这行代码就是真实发往broker的request头,根据END_TRANSACTION就可以在broker端找到broker的处理逻辑
DefaultMQProducerImpl.this.mQClientFactory.getMQClientAPIImpl().endTransactionOneway(brokerAddr, thisHeader, remark,
3000);
}
实际上从生产者端的代码能得到的内容不多还得看broker是如何处理的
Broker处理事务状态
java
// 注册事件处理方法
this.remotingServer.registerProcessor(RequestCode.END_TRANSACTION, endTransactionProcessor, this.endTransactionExecutor);
public class EndTransactionProcessor implements NettyRequestProcessor {
...
// 实际上broker只处理回滚或者commit,不处理UNKNOW
if (MessageSysFlag.TRANSACTION_COMMIT_TYPE == requestHeader.getCommitOrRollback()) {
// commitMessage代码在下面代码1中,实际上只是获取了要commit的消息
result = this.brokerController.getTransactionalMessageService().commitMessage(requestHeader);
...
// endMessageTransaction代码在下面的代码2中,实际上是拿到了真是的topic信息
MessageExtBrokerInner msgInner = endMessageTransaction(result.getPrepareMessage());
// 清理掉事务消息的标记
MessageAccessor.clearProperty(msgInner, MessageConst.PROPERTY_TRANSACTION_PREPARED);
RemotingCommand sendResult = sendFinalMessage(msgInner);
if (sendResult.getCode() == ResponseCode.SUCCESS) {
// deletePrepareMessage代码分析在下面的代码3中,
this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage());
}
} else if (MessageSysFlag.TRANSACTION_ROLLBACK_TYPE == requestHeader.getCommitOrRollback()) {
// rollback和commit之间的区别就是是否写入消息到真实topic
this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage());
}
}
这里记录代码1:commitMessage
java
// 代码1:commitMessage
// 这里其实没有真实的commitMessage,实际上只是从RMQ_SYS_TRANS_HALF_TOPIC获取到了消息
public OperationResult commitMessage(EndTransactionRequestHeader requestHeader) {
return getHalfMessageByOffset(requestHeader.getCommitLogOffset());
}
private OperationResult getHalfMessageByOffset(long commitLogOffset) {
OperationResult response = new OperationResult();
// 只看这一行就行,获取了一个MessageExt
MessageExt messageExt = this.transactionalMessageBridge.lookMessageByOffset(commitLogOffset);
if (messageExt != null) {
response.setPrepareMessage(messageExt);
response.setResponseCode(ResponseCode.SUCCESS);
} else {
response.setResponseCode(ResponseCode.SYSTEM_ERROR);
response.setResponseRemark("Find prepared transaction message failed");
}
return response;
}
这里记录代码2:endMessageTransaction
java
private MessageExtBrokerInner endMessageTransaction(MessageExt msgExt) {
// 这里就拿到真实的topic
msgInner.setTopic(msgExt.getUserProperty(MessageConst.PROPERTY_REAL_TOPIC));
...
}
这里记录代码3:deletePrepareMessage
java
private final ConcurrentHashMap<Integer, MessageQueueOpContext> deleteContext = new ConcurrentHashMap<>();
---
public class MessageQueueOpContext {
// 阻塞队列
private LinkedBlockingQueue<String> contextQueue;
}
public boolean deletePrepareMessage(MessageExt messageExt) {
// <queueID:offset1,offset2,offset3>
// deleteContext实际上是以队列ID(真实写入的topic的队列ID)作为key,queueOffset按照`,`作为分割符拼接而成的,这样就是每次处理一批消息而不是一条消息
Integer queueId = messageExt.getQueueId();
MessageQueueOpContext mqContext = deleteContext.get(queueId);
...
// OFFSET_SEPARATOR = ","
String data = messageExt.getQueueOffset() + TransactionalMessageUtil.OFFSET_SEPARATOR;
boolean res = mqContext.getContextQueue().offer(data, 100, TimeUnit.MILLISECONDS);
if (res) {
int totalSize = mqContext.getTotalSize().addAndGet(data.length());
if (totalSize > transactionalMessageBridge.getBrokerController().getBrokerConfig().getTransactionOpMsgMaxSize()) {
// 这里是一个批处理,真实的处理deletePrepare的类transactionalOpBatchService,这里我们在代码4中分析transactionalOpBatchService的作用
this.transactionalOpBatchService.wakeup();
}
return true;
}
....
下面的代码是在没有添加成功阻塞队列的一个容错处理
}
代码3: transactionalOpBatchService对应TransactionalOpBatchService类
java
// 核心方法是TransactionalMessageServiceImpl类中的batchSendOpMessage方法
public long batchSendOpMessage() {
...
// 这个方法里出现了RMQ_SYS_TRANS_OP_HALF_TOPIC,打了一个标记我们后面从dashboard的消息可以看到是d表示delete
// return new Message(opTopic, TransactionalMessageUtil.REMOVE_TAG,sb.toString().getBytes(TransactionalMessageUtil.CHARSET));
// 实际最终写入RMQ_SYS_TRANS_OP_HALF_TOPIC的消息体是每一条commit/rollback的消息的queueoffset按照`,`拼接
Message opMsg = getOpMessage(entry.getKey(), null);
// 写消息
this.transactionalMessageBridge.writeOp(entry.getKey(), entry.getValue()
}
到这里整个事务消息的流程就已经梳理清楚了,我们先broker在收到生产者发过来的事务状态后做了:
- 从RMQ_SYS_TRANS_HALF_TOPIC中获取到消息
- 根据事务状态COMMIT/ROLLBACK选择是否将消息写入真实的topic,UNKNOW状态我们这里是不处理的
- 将已经commit/rollback的消息的queueOffset作为消息体,写入到RMQ_SYS_TRANS_OP_HALF_TOPIC中,至此事务消息处理完毕
我们来简单画个图来整理这个过程
这里最终写入到RMQ_SYS_TRANS_OP_HALF_TOPIC中的方法其实是有着相同queueID的消息的queueOffset
我们最后在用一个例子来模拟一下RMQ_SYS_TRANS_HALF_TOPIC和RMQ_SYS_TRANS_OP_HALF_TOPIC中的数据形式
假设我们有两个两个队列的topic分别为topic1
和topic2
,然后这两个topic一起写事务消息在第一阶段他们都会写入RMQ_SYS_TRANS_HALF_TOPIC中,这个时候他们在RMQ_SYS_TRANS_HALF_TOPIC中的形式应该是下图所示,红色方块代表Message。
这个时候他们会执行executeLocalTransaction,有些消息会被commit,有些会rollback,有些就会是UNKNOW(这种我们这里先不讨论),这里假设就是两种,commit or rollback,那么最终在topic1和topic2中的消息就有可能是这样的。
topic1中的消息都看起来都commit了
topic2中queueID=2的消息的少了queueOffset=2的那就说明这条消息的事务状态可能就是rollback的,当然这些都是我构造出来的。
最终RMQ_SYS_TRANS_OP_HALF_TOPIC中保存的消息的body可能如上图所示,我们可以看到他们能被放在一个Message中是因为他们的QueueID一致,和topic无关,保存的内容就是他们的QueueOffset
以上图解纯属个人理解。
checkLocalTransaction
在上文中我们好像都没有看到checkLocalTransaction方法的身影,这里就有疑问了,如果消息返回了UNKNOW,或者说执行中抛出了RunTimeException,executeLocalTransaction中返回的是null怎么办,这样就不会调用endMessageTransaction了,这里就是checkLocalTransaction发挥的时候,除了主动提交事务,还会有一个定时任务负责定时检查是否有事务消息需要处理!
具体的代码逻辑是在TransactionalMessageServiceImpl类的check方法,这里直接看代码实在是难以理解,所以我们设计两种场景,并且在check方法中加上一些日志来具体分析
场景一:exec中直接commit
执行完以后我得到了这样一个SendResult
ini
SendResult [sendStatus=SEND_OK, msgId=7F00000133FC5C8DA9624CEB5ECD0000, offsetMsgId=null, messageQueue=MessageQueue [topic=tranTest, brokerName=192.168.0.103, queueId=4], queueOffset=94]
queueOffset=94
, msgId=7F00000133FC5C8DA9624CEB5ECD0000
RMQ_SYS_TRANS_OP_HALF_TOPIC
我们能看到commit的消息成功写入了RMQ_SYS_TRANS_OP_HALF_TOPIC中,并且body中的QueueOffset是94
RMQ_SYS_TRANS_HALF_TOPIC
也没有什么问题,成功写入的,结合日志和代码分析
java
public void check(long transactionTimeout, int transactionCheckMax,
AbstractTransactionalMessageCheckListener listener) {
// transactionTimeout=60 transactionCheckMax=15
String topic = TopicValidator.RMQ_SYS_TRANS_HALF_TOPIC;
// 这里RMQ_SYS_TRANS_HALF_TOPIC就是一个单队列topic只有一个MessageQueue, for循环我就省略了
Set<MessageQueue> msgQueues = transactionalMessageBridge.fetchMessageQueues(topic);
// 获取到RMQ_SYS_TRANS_OP_HALF_TOPIC
MessageQueue opQueue = getOpQueue(messageQueue);
// 获取当前的Offset, 这里目前有一个疑惑的地方GroupName=CID_SYS_RMQ_TRANS,这个offset是什么时候更新的
long halfOffset = transactionalMessageBridge.fetchConsumeOffset(messageQueue);
long opOffset = transactionalMessageBridge.fetchConsumeOffset(opQueue);
// log.info("Before check, the queue={} msgOffset={} opOffset={}", messageQueue, halfOffset, opOffset);
// 这里日志的值为msgOffset=94 opOffset=23
List<Long> doneOpOffset = new ArrayList<>();
HashMap<Long, Long> removeMap = new HashMap<>();
HashMap<Long, HashSet<Long>> opMsgMap = new HashMap<Long, HashSet<Long>>();
// 去RMQ_SYS_TRANS_OP_HALF_TOPIC 起始offset 23查消息
PullResult pullResult = fillOpRemoveMap(removeMap, opQueue, opOffset, halfOffset, opMsgMap, doneOpOffset);
// log.info("193 getMessageNullCount={} newOffset={} nextOpOffset={} removeMap={}, doneOpOffset={}, opMsgMap={}", getMessageNullCount, newOffset, nextOpOffset, removeMap, doneOpOffset, opMsgMap);
i=halfOffset //94
// 从RMQ_SYS_TRANS_OP_HALF_TOPIC中查到了offset=23的消息
// getMessageNullCount=1 newOffset=94 nextOpOffset=24 removeMap={94=23}, doneOpOffset=[], opMsgMap={23=[94]}
if (removeMap.containsKey(i)) {
// 走到了这里的逻辑我们就不看else了,等会下面看else
// log.info("Half offset {} has been committed/rolled back", i);
Long removedOpOffset = removeMap.remove(i);
opMsgMap.get(removedOpOffset).remove(i);
if (opMsgMap.get(removedOpOffset).size() == 0) {
opMsgMap.remove(removedOpOffset);
doneOpOffset.add(removedOpOffset);
}
}
...一大堆的消息时间校验
// 这里我们不需要执行check方法,因为我们已经在ophalf里查到了消息
// 具体的判断需不需要执行check的逻辑如下
List<MessageExt> opMsg = pullResult == null ? null : pullResult.getMsgFoundList();
boolean isNeedCheck = opMsg == null && (valueOfCurrentMinusBorn > checkImmunityTime
|| opMsg != null && opMsg.get(opMsg.size() - 1).getBornTimestamp() - startTime > transactionTimeout
|| valueOfCurrentMinusBorn <= -1);
// 下面这段感觉没有什么看的又去ophalf里查了一次,offset设置成了24,很明显应该是null
// 这两行比较关键,newoffset此时是95了
newOffset = i + 1;
i++;
if (newOffset != halfOffset) {
// 有一种豁然开朗的感觉,RMQ_SYS_TRANS_HALF_TOPIC的位点提交!!!下一次通过transactionalMessageBridge.fetchConsumeOffset取到的位点就是95了
transactionalMessageBridge.updateConsumeOffset(messageQueue, newOffset);
}
// RMQ_SYS_TRANS_OP_HALF_TOPIC 位点提交!!下次通过transactionalMessageBridge.fetchConsumeOffset取到的位点是24
long newOpOffset = calculateOpOffset(doneOpOffset, opOffset);
if (newOpOffset != opOffset) {
transactionalMessageBridge.updateConsumeOffset(opQueue, newOpOffset);
}
// 再往后又去这两个topic中根据新的位点查了一次消息,结果就是没有查到
}
至此!所有正常commit/Rollback的部分全部分析完毕! 在正常commit/checkLocalTransaction,并且会在check中提交RMQ_SYS_TRANS_HALF_TOPIC和RMQ_SYS_TRANS_OP_HALF_TOPIC的位点信息
场景二:executeLocalTransaction中返回UNKNOW
ini
SendResult [sendStatus=SEND_OK, msgId=7F0000017EE66A6824BE4D2BB7330000, offsetMsgId=null, messageQueue=MessageQueue [topic=tranTest, brokerName=192.168.0.103, queueId=13], queueOffset=96]
What?! SendResult中返回的QueueOffset是96,但是最终保存到RMQ_SYS_TRANS_OP_HALF_TOPIC中的QueueOffset是97,我们带着这个疑问去看代码,我们首先先梳理下我们当前的信息
- 消息成功commit了 (说明成功执行check方法了)
- trantest中能够查到这条消息
- 消息保存到RMQ_SYS_TRANS_OP_HALF_TOPIC中QueueOffset有增加
其实check方法上面我们已经了解的差不多了,我们就看看区别的地方
java
long halfOffset = transactionalMessageBridge.fetchConsumeOffset(messageQueue);
long opOffset = transactionalMessageBridge.fetchConsumeOffset(opQueue);
// 这里的pullRequest是null,因为此时消息还没有commit也就没有写入RMQ_SYS_TRANS_OP_HALF_TOPIC
PullResult pullResult = fillOpRemoveMap(removeMap, opQueue, opOffset, halfOffset, opMsgMap, doneOpOffset);
下面我们来看下上文中代码块24行那里说的else
java
// i=96,getResult就不为null了
GetResult getResult = getHalfMsg(messageQueue, i);
// 此时就需要执行本地事务中的check方法了
if (isNeedCheck) {
// 这里重新把消息放回HalfMsgQueue,这里很有可能是QueueOffset增加的原因
// 确实transactionalMessageBridge.putMessageReturnResult(msgInner);这里最终重新向RMQ_SYS_TRANS_HALF_TOPIC中放了一条消息从下图中就可以看到
if (!putBackHalfMsgQueue(msgExt, i)) {
continue;
}
putInQueueCount++;
// 执行本地check方法
// 发送给客户端RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CHECK_TRANSACTION_STATE, requestHeader);
// 然后会执行doExecuteEndTransactionHook(msg, uniqueKey, brokerAddr, localTransactionState, true);
listener.resolveHalfMsg(msgExt);
}
当执行doExecuteEndTransactionHook后剩下的就和上文中的commit/rollback一样了,如果在check中还返回了UNKNOW,那就会一直循环这个过程直到事务消息超时或者超过一定的次数
开发、运维过程中使用和排查事务消息
首先说下开发过程中吧,还是用上面的写DB和写RocketMQ的例子
scss
transaction {
updateDB()
success = sendMessage()
if success {
commit()
}
rollback()
}
这里我们就可以先加上事务消息了,我们将这些操作移动到executeLocalTransaction方法中
java
public LocalTransactionState executeLocalTransaction(Message message, Object o) {
// 需要执行的内容
System.out.println("executeLocalTransaction");
try {
updateDB();
putES();
putLogFile();
} catch Exception e {
return LocalTransactionState.ROLLBACK;
}
return LocalTransactionState.COMMIT;
}
上面的内容我又加了一些操作,这些操作都要是一个整体,这里其实已经能看出来问题了,我们的回滚方法只有RocketMQ具备,在RocketMQ回滚的时候别的方法实际上并没有回滚的能力,这里我们应该对每一个操作都要有回滚的方法。
java
public LocalTransactionState executeLocalTransaction(Message message, Object o) {
// 需要执行的内容
System.out.println("executeLocalTransaction");
try {
updateDB();
putES();
putLogFile();
} catch Exception e {
// rollback中针对DB,es,logfile都要能够回滚并且是幂等的
rollback();
return LocalTransactionState.ROLLBACK;
}
return LocalTransactionState.COMMIT;
}
这个其实是我对于事务消息用于开发时的一个感想
再说说运维场景,主要就是需要查这个事务消息的状态,这个事务消息是commit还是rollback,有没有真实的写入到real topic中
这些都可以根据dashboard或者mqadmin去查到,具体遇到事务消息的运维场景还不多,后面可以补充 具体排查时可以查broker端的transaction.log文件,broker.log中其实也能反应一部分的问题,像我这次其实触发了一个问题
我在check方法还没有执行完的时候就把我的生产者shutdown了,导致broker.log里就能看到channel已经关闭了,但是broker还想调用本地的事务check方法