Comparison of Transaction Message Implementations in ZuanZuan's Custom RocketMQ Version and the Open‑Source Community Version
This article analyzes why ZuanZuan built its own RocketMQ transaction‑message solution, compares the custom implementation with the community edition’s two‑phase commit approach, explains the underlying principles, sending flow, failure handling, and unknown‑state processing, and summarizes the trade‑offs of each design.
When using RocketMQ at the company, it was discovered that transactional messages were not provided by the open‑source community edition but by an internally developed version, prompting curiosity about the reasons and differences.
The internal ZuanZuan version (RocketMQ 3.4.6, first transaction support in Dec 2017) predates the community support which only appeared in version 4.3.0 (Aug 2018). The custom solution places transaction handling pressure on the producer side, requiring a transaction table in the producer’s MySQL database.
2.1 Basic Principle (Custom Version) – The producer creates a transaction message table in MySQL and commits or rolls back the message together with the local DB transaction.
Example code:
@Transactional(rollbackFor = Exception.class)
public void testTransaction() throws Exception {
SingleInboundReq inboundReq = new SingleInboundReq(); // 入库实体
inboundService.singleInbound(inboundReq); // 本地事务操作 提交入库
InventoryOrderMsg inventoryOrderMsg = new InventoryOrderMsg(); // 消息内容
mqProduceComponent.sendCisAddMq(inventoryOrderMsg); //发送事务消息 同步入库信息
}Message sending is combined with the local transaction, then placed into an in‑memory queue for asynchronous processing.
2.2 How to Send Messages – The message and its primary key are stored in a queue; basic validation (retry count, interval, etc.) is performed before sending to the broker.
If sending fails, the message is moved to a timeWheelQueue and retried every 5 ms with exponential back‑off intervals (0, 5, 10, 25, 50, 100, 200, 300, 500, 800, 1000 ms).
2.3 Handling Unsent Messages – A background thread periodically (every 10 seconds) scans messages older than 10 seconds, resends them to the broker, and deletes the local record.
3.1 Basic Principle (Community Version) – Implements a two‑phase commit: first a half message is sent and the local transaction executed; later an op message indicates commit or rollback.
Half messages are stored in RMQ_SYS_TRANS_HALF_TOPIC ; op messages are stored in RMQ_SYS_TRANS_OP_HALF_TOPIC with tag d to signal the final state.
3.2 How to Send Messages – The real message’s topic, queueId, and tags are placed in the properties, then the message is sent to the half‑topic. After the broker acknowledges, executeLocalTransaction runs; finally endTransaction writes the op message.
3.3 Handling Unknown‑State Messages – The broker periodically (every 60 seconds) checks half messages without corresponding op messages. It fetches consumption positions, builds a removal map, and retries or discards messages based on retry count ( BrokerConfig.transactionCheckMax ) and age ( MessageStoreConfig.fileReservedTime ).
Overall, the custom ZuanZuan version pushes transaction logic to the producer, while the community version follows the standard two‑phase commit model with broker‑side checks; each has its own advantages and trade‑offs depending on the scenario.
The article concludes that different technical solutions fit different contexts, and the choice should be driven by practical requirements rather than theoretical superiority.
Zhuanzhuan Tech
A platform for Zhuanzhuan R&D and industry peers to learn and exchange technology, regularly sharing frontline experience and cutting‑edge topics. We welcome practical discussions and sharing; contact waterystone with any questions.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.