Distributed Transaction Solutions: Two‑Phase Commit, Message Queues, and RocketMQ Implementation
The article explains how to guarantee data consistency across distributed systems by using local transactions, two‑phase commit protocols, message‑queue based eventual consistency, deduplication techniques, and practical RocketMQ transactional messaging code examples in Java.
When a user transfers money from an Alipay account to a YuEBao account, the system must ensure that both the debit and the credit succeed; otherwise data inconsistency occurs. This problem appears in many domains such as e‑commerce order processing and advertising billing.
Fundamentally, the issue can be abstracted as: after updating one table, another table must also be updated successfully. In a single‑machine setup, a local transaction can solve this problem easily.
For large‑scale systems where the involved tables reside on different database instances, local transactions are insufficient. Two mainstream solutions are presented:
1. Distributed Transactions – Two‑Phase Commit (2PC)
2PC involves a coordinator (TC) and multiple participants (Si). The coordinator first writes a prepare log, then asks each participant to prepare. Participants execute their local transaction but do not commit, returning yes or no . If all participants return yes , the coordinator sends a commit message; otherwise it sends an abort . Logs are written at each step to enable recovery after failures.
Although 2PC guarantees atomicity, it suffers from high latency and long lock times, making it unsuitable for high‑concurrency services.
2. Using Message Queues to Avoid Distributed Transactions
Inspired by real‑world scenarios (e.g., a restaurant issuing a ticket before serving food), the article proposes storing a reliable “ticket” (message) that records the intent to credit the target account. Two patterns are discussed:
Coupled Business‑Message Model : The debit transaction and message insertion occur in the same local transaction, ensuring that if the debit succeeds, the message is persisted.
Decoupled Business‑Message Model : The message is first persisted independently; the business transaction proceeds only after the message is confirmed, and vice‑versa. This reduces coupling but requires additional status‑check logic.
Message deduplication is handled by a message_apply table that records processed message IDs; before processing a message, the system checks this table to discard duplicates.
3. Splitting Large Transactions into Small Asynchronous Units
Large transactions can be broken into a local debit transaction and an asynchronous message that triggers the credit. The article discusses the pitfalls of sending the message before the debit (possible credit without debit) and sending the message after the debit (possible debit without credit), and suggests embedding the message send within the same transaction when possible.
4. RocketMQ Transactional Messaging
RocketMQ implements a three‑stage process: send a Prepared message, execute the local transaction, then send a commit/rollback based on the transaction outcome. The broker periodically checks unresolved Prepared messages and invokes the producer’s TransactionCheckListener to decide the final state.
Key code snippets (preserved in ... tags) illustrate the producer setup, transaction listener implementation, and consumer handling:
TransactionMQProducer producer = new TransactionMQProducer("groupName");
producer.setTransactionCheckListener(new TransactionCheckListenerImpl());
// ...
SendResult sendResult = producer.sendMessageInTransaction(msg, tranExecuter, null); public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
int status = transactionIndex.getAndIncrement() % 3;
switch (status) {
case 0: return LocalTransactionState.UNKNOW;
case 1: return LocalTransactionState.COMMIT_MESSAGE;
case 2: return LocalTransactionState.ROLLBACK_MESSAGE;
}
return LocalTransactionState.COMMIT_MESSAGE;
}The consumer registers a MessageListenerOrderly that processes messages, acknowledges automatically, and retries on failure.
5. Ensuring End‑to‑End Consistency
Even with transactional messaging, failures can cause mismatched debit/credit. The article recommends a reconciliation process: after a transaction message is sent, a non‑transactional reconciliation message is published to a dedicated topic. A downstream reconciliation service compares the state of the producer and consumer; if the consumer failed while the producer succeeded, the service rolls back the debit.
Overall, the article provides a comprehensive guide to handling distributed consistency using 2PC, message‑queue based eventual consistency, deduplication, and practical RocketMQ transactional messaging patterns.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.
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.