rocketmq transaction-事務消息與事務回查

文章基於rocket-mq4.3 代碼分析

事務消息發送

事務消息主要解決的問題是:本地事務與消息發送原子性的問題,通過消息異步消費(直到消費完成)達到生產端與消費端應用事務最終一致性。

如果要是有事務消息需要使用  TransactionMQProducer 這個類,下面摘自RocketMq源碼裏的測試例子

public class TransactionProducer {
    public static void main(String[] args) throws MQClientException, InterruptedException {
        TransactionListener transactionListener = new TransactionListenerImpl();
        TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name");
        ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2000), new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r);
                thread.setName("client-transaction-msg-check-thread");
                return thread;
            }
        });

        producer.setExecutorService(executorService);

//設置一個監聽,用戶本地事務的處理與接收broker超時回查
        producer.setTransactionListener(transactionListener);
        producer.start();

        String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
        for (int i = 0; i < 10; i++) {
            try {
                Message msg =
                    new Message("TopicTest1234", tags[i % tags.length], "KEY" + i,
                        ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
                SendResult sendResult = producer.sendMessageInTransaction(msg, null);
                System.out.printf("%s%n", sendResult);

                Thread.sleep(10);
            } catch (MQClientException | UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }

//這裏sleep是爲了保證client能夠接收到broker的回調
        for (int i = 0; i < 100000; i++) {
            Thread.sleep(1000);
        }
        producer.shutdown();
    }
}
TransactionListenerImpl 實現了 TransactionListener,該接口有兩個方法
public class TransactionListenerImpl implements TransactionListener {
    private AtomicInteger transactionIndex = new AtomicInteger(0);

    private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>();

//這個方法會在client端發送完消息後根據消息發送的結果調用,是被client端自己調用的;
//這個方法被執行,表示client與broker之間是網絡暢通的,該方法主要編寫的功能是完成本地事務的處理
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        int value = transactionIndex.getAndIncrement();
        int status = value % 3;
        localTrans.put(msg.getTransactionId(), status);
        return LocalTransactionState.UNKNOW;
    }

//這個方法是broker在超時後主動調用的,用於檢查本地事務是否已經完成,brokerg根據返回值決定是否需要投遞消息
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        Integer status = localTrans.get(msg.getTransactionId());
        if (null != status) {
            switch (status) {
                case 0:
                    return LocalTransactionState.UNKNOW;
                case 1:
                    return LocalTransactionState.COMMIT_MESSAGE;
                case 2:
                    return LocalTransactionState.ROLLBACK_MESSAGE;
            }
        }
        return LocalTransactionState.COMMIT_MESSAGE;
    }
}

摘自網上一個事務消息處理流程圖:

在發送消息階段,事務消息會被加上一點標記,這是它與普通消息差異的地方

MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true");
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_PRODUCER_GROUP, this.defaultMQProducer.getProducerGroup());

broker在接收到這樣的消息後會調用

org.apache.rocketmq.broker.processor.SendMessageProcessor#sendMessage方法即消息通用保存流程(後續會有flushDisk、HA、cousumeQueus數據增加,索引重建等動作)

 

private RemotingCommand sendMessage(final ChannelHandlerContext ctx,
                                        final RemotingCommand request,
                                        final SendMessageContext sendMessageContext,
                                        final SendMessageRequestHeader requestHeader) throws RemotingCommandException {

        
//省略一大波代碼........

//校驗是否有事務消息屬性 PROPERTY_TRANSACTION_PREPARED
        String traFlag = oriProps.get(MessageConst.PROPERTY_TRANSACTION_PREPARED);
        if (traFlag != null && Boolean.parseBoolean(traFlag)) {

//如果broker不接收事務消息,設置備註,不處理直接返回結果
if (this.brokerController.getBrokerConfig().isRejectTransactionMessage()) {
                response.setCode(ResponseCode.NO_PERMISSION);
                response.setRemark("the broker[" + this.brokerController.getBrokerConfig().getBrokerIP1() + "] sending transaction message is forbidden");
                return response;
            }

//保存消息(調用的是TransactionalMessageServiceImpl實現類)
            putMessageResult = this.brokerController.getTransactionalMessageService().prepareMessage(msgInner);
        } else {
            putMessageResult = this.brokerController.getMessageStore().putMessage(msgInner);
        }

        return handlePutMessageResult(putMessageResult, response, request, msgInner, responseHeader, sendMessageContext, ctx, queueIdInt);

    }

TransactionalMessageServiceImpl然後調用org.apache.rocketmq.broker.transaction.queue.TransactionalMessageBridge#parseHalfMessageInner方法

 

//調用defaultmesgstroe保存消息    
public PutMessageResult putHalfMessage(MessageExtBrokerInner messageInner) {
        return store.putMessage(parseHalfMessageInner(messageInner));
    }

//將原有消息蓋頭換面
    private MessageExtBrokerInner parseHalfMessageInner(MessageExtBrokerInner msgInner) {

//將原始消息的topic和queueid都備份出來        
MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_TOPIC, msgInner.getTopic());
        MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_QUEUE_ID,String.valueOf(msgInner.getQueueId()));
        msgInner.setSysFlag(MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), MessageSysFlag.TRANSACTION_NOT_TYPE));

//設置新的topic和queueId
        msgInner.setTopic(TransactionalMessageUtil.buildHalfTopic());
        msgInner.setQueueId(0);
        msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties()));
        return msgInner;
    }

後面就會走正常流程將消息保存進入commitlog中,保存的topic名稱是 RMQ_SYS_TRANS_HALF_TOPIC,queueId是0;

消息保存完畢後,broker向client返回結果,client接收到結果後根據這個結果判斷是否要執行本地事務;

 

//如果broker返回消息保存成功的消息
switch (sendResult.getSendStatus()) {
            case SEND_OK: {
                try {
                    if (sendResult.getTransactionId() != null) {
                        msg.putUserProperty("__transactionId__", sendResult.getTransactionId());
                    }
                    String transactionId = msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX);
                    if (null != transactionId && !"".equals(transactionId)) {
                        msg.setTransactionId(transactionId);
                    }
                    if (null != localTransactionExecuter) {
                        localTransactionState = localTransactionExecuter.executeLocalTransactionBranch(msg, arg);
                    } else if (transactionListener != null) {

//調用transactionListener執行client端本地事務,並獲得返回值 localTransactionState
                        log.debug("Used new transaction API");
                        localTransactionState = transactionListener.executeLocalTransaction(msg, arg);
                    }
                    if (null == localTransactionState) {
                        localTransactionState = LocalTransactionState.UNKNOW;
                    }

                    if (localTransactionState != LocalTransactionState.COMMIT_MESSAGE) {
                        log.info("executeLocalTransactionBranch return {}", localTransactionState);
                        log.info(msg.toString());
                    }
                } catch (Throwable e) {
                    log.info("executeLocalTransactionBranch exception", e);
                    log.info(msg.toString());
                    localException = e;
                }
            }
            break;
            case FLUSH_DISK_TIMEOUT:
            case FLUSH_SLAVE_TIMEOUT:
            case SLAVE_NOT_AVAILABLE:
                localTransactionState = LocalTransactionState.ROLLBACK_MESSAGE;
                break;
            default:
                break;
        }

接下來就是事務的收尾階段,調用的流程是:

org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#endTransaction
↓
org.apache.rocketmq.client.impl.MQClientAPIImpl#endTransactionOneway(RequestCode爲:RequestCode.END_TRANSACTION)
↓
org.apache.rocketmq.remoting.netty.NettyRemotingClient#invokeOneway

調用流程來到broker端,處理該調用的Processor是org.apache.rocketmq.broker.processor.EndTransactionProcessor

我們分析一下事務提交部分的代碼,回滾的代碼先不分析

if (MessageSysFlag.TRANSACTION_COMMIT_TYPE == requestHeader.getCommitOrRollback()) {
            result = this.brokerController.getTransactionalMessageService().commitMessage(requestHeader);
            if (result.getResponseCode() == ResponseCode.SUCCESS) {
                RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader);
                if (res.getCode() == ResponseCode.SUCCESS) {
                    MessageExtBrokerInner msgInner = endMessageTransaction(result.getPrepareMessage());
                    msgInner.setSysFlag(MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), requestHeader.getCommitOrRollback()));
                    msgInner.setQueueOffset(requestHeader.getTranStateTableOffset());
                    msgInner.setPreparedTransactionOffset(requestHeader.getCommitLogOffset());
                    msgInner.setStoreTimestamp(result.getPrepareMessage().getStoreTimestamp());

//將消息原有的屬性恢復並重新執行sendMessage的流程
                    RemotingCommand sendResult = sendFinalMessage(msgInner);
                    if (sendResult.getCode() == ResponseCode.SUCCESS) {
                        this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage());
                    }
                    return sendResult;
                }
                return res;
            }
        }

我們看到這裏的做法跟定時消息處理的機制差不多,都是先將消息查詢出來,恢復原有的屬性(topic,queueid等)再將消息保存一遍,這樣消費端就能消費了。

事務回查

在broker的 initialize() 方法最後的地方會設置事務消息回查相關的服務類

private void initialTransaction() {
        this.transactionalMessageService = ServiceProvider.loadClass(ServiceProvider.TRANSACTION_SERVICE_ID, TransactionalMessageService.class);
        if (null == this.transactionalMessageService) {
            this.transactionalMessageService = new TransactionalMessageServiceImpl(new TransactionalMessageBridge(this, this.getMessageStore()));
            log.warn("Load default transaction message hook service: {}", TransactionalMessageServiceImpl.class.getSimpleName());
        }
        this.transactionalMessageCheckListener = ServiceProvider.loadClass(ServiceProvider.TRANSACTION_LISTENER_ID, AbstractTransactionalMessageCheckListener.class);
        if (null == this.transactionalMessageCheckListener) {
            this.transactionalMessageCheckListener = new DefaultTransactionalMessageCheckListener();
            log.warn("Load default discard message hook service: {}", DefaultTransactionalMessageCheckListener.class.getSimpleName());
        }
        this.transactionalMessageCheckListener.setBrokerController(this);
        this.transactionalMessageCheckService = new TransactionalMessageCheckService(this);
    }

TransactionalMessageCheckService是一個線程類,最終會不斷執行 onWaitEnd() 方法

@Override
    protected void onWaitEnd() {
        long timeout = brokerController.getBrokerConfig().getTransactionTimeOut();
        int checkMax = brokerController.getBrokerConfig().getTransactionCheckMax();
        long begin = System.currentTimeMillis();
        log.info("Begin to check prepare message, begin time:{}", begin);
        this.brokerController.getTransactionalMessageService().check(timeout, checkMax, this.brokerController.getTransactionalMessageCheckListener());
        log.info("End to check prepare message, consumed time:{}", System.currentTimeMillis() - begin);
    }

即最終執行的是:org.apache.rocketmq.broker.transaction.queue.TransactionalMessageServiceImpl#check 方法

該方法會不斷的通過固定的topic  MixAll.RMQ_SYS_TRANS_HALF_TOPIC 組裝對應的 MessageQueue集合(根據該topic對應的配置 topicConfig.getReadQueueNums | 其實我對rocketmq topic配置信息裏爲什麼把readQueueNum和writeQueueNum分開,而不使用同一個數字感到不解),然後做事務回邏輯;

 

下面是《RocketMQ技術內幕》作者給我的解答:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章