RocketMQ Transactional Messaging in Practice: From Half Message to Production‑Grade Consistency
The article analyses why sending a message is easy but guaranteeing end‑to‑end consistency across databases, brokers, networks and services is hard, explains the exact problems RocketMQ transactional messages solve, compares them with Outbox and CDC, and provides a complete production‑grade design, implementation, monitoring and tuning guide.
Why This Article Matters
The real difficulty is not "sending a message" but keeping business state provable, message results traceable, and exception paths compensable when databases, brokers, networks, or application instances may fail at any moment. RocketMQ transactional messages address a key breakpoint in this chain, but they are not a silver bullet for distributed transactions, state machines, idempotency, or reconciliation.
1. Scope of Transactional Messages
1.1 What It Guarantees
Transactional messages ensure atomicity between the local transaction commit result and the message visibility to consumers. In production, the rule is:
If the local transaction succeeds, the message must be committed and consumable.
If the local transaction fails, the message must be rolled back and invisible.
If a network break, process crash, or broker loss occurs, the broker’s check‑back mechanism fills the gap.
1.2 What It Does Not Solve
It does not guarantee exactly‑once consumption.
It does not provide global strong consistency across multiple downstream services.
It does not enforce ordering of downstream business.
It does not replace business state machines, compensation, reconciliation, or dead‑letter handling.
It is unsuitable for long‑running, manual‑approval, or hour‑scale transactions.
RocketMQ transactional messages guarantee "whether a message should be sent", not "the whole business chain succeeds once".1.3 Comparison with Outbox / Local Message Table / CDC
Solution | Solved Focus | Send Timing | Advantages | Limitations
-------------------|----------------------------------|----------------------------------|-----------------------------------|-------------------------------
RocketMQ Tx Msg | Bind local transaction & MQ visibility | Driven by RocketMQ two‑phase flow | Broker native support, clear check‑back | Strong coupling to RocketMQ, need to manage check‑back logic
Outbox / Local Msg | Atomicity of DB write & event persistence | Transaction commit → async scan | Decouples MQ, easier multi‑downstream, CDC friendly | Longer chain, larger ops surface
CDC + MQ | Extract events from DB logs | Transaction commit → CDC capture | Low intrusion, platform‑friendly | More complex latency, ordering, deduplication, semanticsIn practice:
If you are deep in the RocketMQ ecosystem and need a direct, efficient way to ensure "after a single business transaction, an event must be reliably emitted", transactional messages are appropriate.
If you need fan‑out, multi‑broker, multi‑protocol, or reuse across many consumption domains, an Outbox approach is often more stable.
If you prioritize platform‑wide low‑intrusion and unified data‑change distribution, CDC is suitable, though it brings higher governance cost.
2. Problem Background: Why Simple "Write DB Then Send Message" Fails
Example: payment success drives order fulfillment.
Payment service receives success confirmation.
Updates payment status to SUCCESS.
Updates order status to PAID.
Publishes OrderPaidEvent.
Downstream services (stock, marketing, points, fulfillment) consume asynchronously.
A naïve implementation wraps the whole flow in a @Transactional method and calls rocketMqTemplate.syncSend(...). This looks atomic but actually leaves three classic failure modes:
2.1 Data Success, Message Failure
Database commit succeeds.
MQ send fails or times out.
Downstream never knows the order is paid → state split.
2.2 Message Success, Data Rollback
Message sent before DB write.
Downstream starts processing (stock, coupons, shipping).
Main transaction rolls back → event is true but facts are false, high compensation cost.
2.3 Producer Timeout but Broker Received
Application thinks the send failed.
Broker already stored the half message.
Retry creates duplicate messages → idempotency burden on consumer.
2.4 Mid‑Process Crash Leaves State Suspended
Half message sent.
Local transaction crashes halfway.
Broker cannot decide commit or rollback → triggers check‑back.
3. Core Mechanics of RocketMQ Transactional Messages
3.1 End‑to‑End Flow
Producer writes a "temporarily invisible" half message to the broker.
Producer executes the local transaction.
Producer reports the transaction result (commit/rollback) to the broker.
Broker makes the message visible or discards it.
If the producer does not give a definitive answer, the broker periodically checks back.
The essence is:
Message existence first.
Visibility later, decided by the local transaction outcome.
3.2 Diagram (textual representation)
Producer Broker Consumer
| 1. send half message --> | | |
| <-- ack ---------------- | | |
| 2. execute local tx ----> | | |
| 3. commit/rollback ----> | decides visibility | |
| | | 4. if unknown, broker initiates check |
| | <-- check request --------------------------- |
| 5. check local tx -----> | | 6. broker decides again ------------>
| | | 7. message becomes visible ---------> |
| | | 8. consumer processes -------------->3.3 Half Message Explained
Broker has received the business payload.
It is not yet part of the normal consumption view.
Only after the producer explicitly commits does it become consumable.
If the local transaction succeeds but the half message never reaches the broker, there is no later compensation space.
3.4 Commit / Rollback Semantics
Message systems excel at append‑only writes; they rarely delete.
Transactional messages never physically delete; they either make the message visible or keep it invisible for later cleanup.
3.5 Check‑Back Importance
Because distributed systems inevitably experience failures, the producer cannot always instantly inform the broker of the final result. Scenarios that trigger a check‑back include:
Producer crashes after local transaction.
Commit/rollback response lost in the network.
Local transaction exceeds the check‑back timeout.
Producer explicitly returns UNKNOW.
The check‑back is a formal path, not an exception.
3.6 Producer Group Role
Producer Group is more than a client name; it is the "check‑back domain". All instances in the same group must share the same transaction‑state view, otherwise inconsistent results appear (e.g., instance A says commit, B says unknown, C says rollback).
4. Architectural Design: From Message Sending to Full Consistency Chain
4.1 Recommended Business Layers
Access Layer -> receives commands (payment success, order creation, registration)
State Layer -> maintains domain state machines (order, payment, stock)
Event Layer -> handles transactional message sending, transaction record, check‑back
Governance Layer -> idempotency, compensation, monitoring, dead‑letter, capacity controlThe transactional message belongs only to the Event Layer; overall stability depends on the combined effort of State, Event, and Governance layers.
4.2 Sample Business Case: Payment Success Drives Fulfillment
PAYMENT_SUCCESS -> ORDER_PAID -> publish OrderPaidEvent -> stock confirm -> grant coupon -> add points -> create fulfillment orderWhy this fits transactional messages:
Main state change is clear: order is paid.
Event semantics are clear ( OrderPaidEvent).
Downstream actions are asynchronous.
No need to wait for all downstream steps before returning.
4.3 Event Model Recommendation
A production‑grade event should contain at least:
{
"eventId": "01JZ9K1H7Y8F5A3KQ2M4N6P8R1",
"eventType": "ORDER_PAID",
"aggregateType": "ORDER",
"aggregateId": "ORD202606300001",
"businessKey": "PAY202606300009",
"occurredAt": "2026-06-30T10:21:33+08:00",
"traceId": "3f9f8fd1f6fb4f1e9d9c9035e4d88f90",
"producer": "trade-service",
"payload": {
"orderNo": "ORD202606300001",
"userId": 1024,
"paidAmount": 29900,
"currency": "CNY",
"paidAt": "2026-06-30T10:21:31+08:00"
}
}Field design principles: eventId – global deduplication and traceability. aggregateId – business root (order number). businessKey – unique id for idempotency (payment number). traceId – end‑to‑end logging, metrics, alerts. payload – only data needed by downstream, not the whole ORM object.
4.4 Transaction Record Table Design
CREATE TABLE `mq_transaction_record` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`transaction_id` VARCHAR(96) NOT NULL COMMENT 'RocketMQ transaction id',
`producer_group` VARCHAR(128) NOT NULL COMMENT 'Producer group',
`topic` VARCHAR(128) NOT NULL COMMENT 'business Topic',
`business_type` VARCHAR(64) NOT NULL COMMENT 'e.g., ORDER_PAID',
`business_key` VARCHAR(128) NOT NULL COMMENT 'orderNo/payNo',
`event_id` VARCHAR(64) NOT NULL COMMENT 'event unique ID',
`state` TINYINT NOT NULL COMMENT '0‑init 1‑committed 2‑rolled_back',
`check_status` TINYINT NOT NULL DEFAULT 0 COMMENT '0‑none 1‑checking 2‑checked',
`check_times` INT NOT NULL DEFAULT 0 COMMENT 'cumulative check count',
`last_check_time` DATETIME NULL,
`remark` VARCHAR(512) DEFAULT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tx_id` (`transaction_id`),
UNIQUE KEY `uk_event_id` (`event_id`),
KEY `idx_business_key` (`business_key`),
KEY `idx_created_state` (`created_at`, `state`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='RocketMQ transaction record table';This table is used not only for the check‑back but also for manual troubleshooting, event tracing, statistics, and offline compensation.
4.5 Consumer Idempotent Table
CREATE TABLE `mq_consume_idempotent` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`consumer_group` VARCHAR(128) NOT NULL,
`biz_type` VARCHAR(64) NOT NULL,
`event_id` VARCHAR(64) NOT NULL,
`status` TINYINT NOT NULL COMMENT '0‑processing 1‑success 2‑failed',
`result_code` VARCHAR(64) DEFAULT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_group_event` (`consumer_group`, `event_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Consume idempotent table';5. Production‑Ready Implementation (Spring Boot)
5.1 Domain Command Object
public record OrderPaidCommand(
String orderNo,
String paymentNo,
Long userId,
Long paidAmount,
Instant paidAt,
String traceId) { }5.2 Event Object
public record OrderPaidEvent(
String eventId,
String eventType,
String aggregateType,
String aggregateId,
String businessKey,
Instant occurredAt,
String traceId,
String producer,
Payload payload) {
public record Payload(
String orderNo,
Long userId,
Long paidAmount,
Instant paidAt) { }
}5.3 Sending Service (only builds message)
@Service
public class OrderPaidTxMessageService {
private final TransactionMQProducer producer;
private final ObjectMapper objectMapper;
public OrderPaidTxMessageService(TransactionMQProducer producer, ObjectMapper objectMapper) {
this.producer = producer;
this.objectMapper = objectMapper;
}
public TransactionSendResult send(OrderPaidCommand command) {
OrderPaidEvent event = EventFactory.buildOrderPaidEvent(command);
Message message = new Message(
"trade.order.paid",
"paid",
event.eventId(),
serialize(event));
message.setKeys(command.orderNo());
message.putUserProperty("eventId", event.eventId());
message.putUserProperty("businessKey", command.paymentNo());
message.putUserProperty("traceId", command.traceId());
message.putUserProperty("CHECK_IMMUNITY_TIME_IN_SECONDS", "20");
return producer.sendMessageInTransaction(message, command);
}
private byte[] serialize(OrderPaidEvent event) {
try { return objectMapper.writeValueAsBytes(event); }
catch (JsonProcessingException ex) { throw new IllegalStateException("serialize order paid event failed", ex); }
}
}5.4 Local Transaction Listener
@Slf4j
@Component
public class OrderPaidTransactionListener implements TransactionListener {
private final TradeDomainService tradeDomainService;
private final MqTransactionRecordRepository txRepository;
public OrderPaidTransactionListener(TradeDomainService tradeDomainService,
MqTransactionRecordRepository txRepository) {
this.tradeDomainService = tradeDomainService;
this.txRepository = txRepository;
}
@Override
public LocalTransactionState executeLocalTransaction(Message message, Object arg) {
OrderPaidCommand command = (OrderPaidCommand) arg;
String transactionId = message.getTransactionId();
String eventId = message.getUserProperty("eventId");
try {
tradeDomainService.confirmPaidAndRecordTransaction(command, transactionId, eventId);
return LocalTransactionState.COMMIT_MESSAGE;
} catch (BizRetryableException ex) {
log.warn("tx local transaction uncertain, orderNo={}, txId={}", command.orderNo(), transactionId, ex);
return LocalTransactionState.UNKNOW;
} catch (Exception ex) {
log.error("tx local transaction failed, orderNo={}, txId={}", command.orderNo(), transactionId, ex);
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}
@Override
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
String transactionId = messageExt.getTransactionId();
String orderNo = messageExt.getKeys();
String traceId = messageExt.getUserProperty("traceId");
try {
MqTransactionRecord record = txRepository.findByTransactionId(transactionId);
if (record != null) {
txRepository.markChecked(transactionId);
if (record.isCommitted()) return LocalTransactionState.COMMIT_MESSAGE;
if (record.isRolledBack()) return LocalTransactionState.ROLLBACK_MESSAGE;
}
TradeOrder order = tradeDomainService.findOrder(orderNo);
if (order == null) {
log.warn("tx check order not found, orderNo={}, txId={}, traceId={}", orderNo, transactionId, traceId);
return LocalTransactionState.UNKNOW;
}
if (order.isPaid()) {
log.info("tx check fallback commit by domain state, orderNo={}, txId={}, traceId={}", orderNo, transactionId, traceId);
return LocalTransactionState.COMMIT_MESSAGE;
}
if (order.isClosed()) {
log.info("tx check fallback rollback by domain state, orderNo={}, txId={}, traceId={}", orderNo, transactionId, traceId);
return LocalTransactionState.ROLLBACK_MESSAGE;
}
return LocalTransactionState.UNKNOW;
} catch (Exception ex) {
log.error("tx check failed, orderNo={}, txId={}, traceId={}", orderNo, transactionId, traceId, ex);
return LocalTransactionState.UNKNOW;
}
}
}5.5 Domain Service (transaction boundary)
@Service
public class TradeDomainService {
private final PaymentRepository paymentRepository;
private final OrderRepository orderRepository;
private final MqTransactionRecordRepository txRepository;
public TradeDomainService(PaymentRepository paymentRepository,
OrderRepository orderRepository,
MqTransactionRecordRepository txRepository) {
this.paymentRepository = paymentRepository;
this.orderRepository = orderRepository;
this.txRepository = txRepository;
}
@Transactional(rollbackFor = Exception.class)
public void confirmPaidAndRecordTransaction(OrderPaidCommand command,
String transactionId,
String eventId) {
Payment payment = paymentRepository.findByPaymentNoForUpdate(command.paymentNo());
if (payment == null) throw new IllegalArgumentException("payment not found: " + command.paymentNo());
if (payment.isSuccess()) { ensureTransactionRecordExists(command, transactionId, eventId); return; }
payment.markSuccess(command.paidAt());
paymentRepository.save(payment);
TradeOrder order = orderRepository.findByOrderNoForUpdate(command.orderNo());
if (order == null) throw new IllegalArgumentException("order not found: " + command.orderNo());
order.markPaid(command.paidAt());
orderRepository.save(order);
txRepository.insertCommitted(transactionId, "trade_tx_group", "trade.order.paid", "ORDER_PAID", command.paymentNo(), eventId);
}
public TradeOrder findOrder(String orderNo) { return orderRepository.findByOrderNo(orderNo); }
private void ensureTransactionRecordExists(OrderPaidCommand command, String transactionId, String eventId) {
if (txRepository.findByTransactionId(transactionId) != null) return;
txRepository.insertCommitted(transactionId, "trade_tx_group", "trade.order.paid", "ORDER_PAID", command.paymentNo(), eventId);
}
}Note the use of for update (row‑level lock) to guarantee single‑threaded state progression (e.g., PAYING → PAID) and avoid duplicate processing.
5.6 Consumer (idempotent execution)
@Slf4j
@Component
public class OrderPaidConsumer implements MessageListenerConcurrently {
private final RewardService rewardService;
private final InventoryService inventoryService;
private final ConsumeIdempotentService idempotentService;
private final ObjectMapper objectMapper;
public OrderPaidConsumer(RewardService rewardService,
InventoryService inventoryService,
ConsumeIdempotentService idempotentService,
ObjectMapper objectMapper) {
this.rewardService = rewardService;
this.inventoryService = inventoryService;
this.idempotentService = idempotentService;
this.objectMapper = objectMapper;
}
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> messages,
ConsumeConcurrentlyContext context) {
for (MessageExt message : messages) {
String eventId = message.getUserProperty("eventId");
String traceId = message.getUserProperty("traceId");
try {
boolean processed = idempotentService.executeOnce("ORDER_PAID", eventId, () -> handleMessage(message, traceId));
if (!processed) log.info("duplicate message ignored, eventId={}, traceId={}", eventId, traceId);
} catch (RetryableConsumeException ex) {
log.warn("consume retry later, eventId={}, traceId={}", eventId, traceId, ex);
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
} catch (Exception ex) {
log.error("consume failed, eventId={}, traceId={}", eventId, traceId, ex);
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
private void handleMessage(MessageExt message, String traceId) {
OrderPaidEvent event = deserialize(message.getBody());
inventoryService.confirmReservedStock(event.payload().orderNo(), traceId);
rewardService.grantPoints(event.payload().orderNo(), event.payload().userId(), traceId);
}
private OrderPaidEvent deserialize(byte[] body) {
try { return objectMapper.readValue(body, OrderPaidEvent.class); }
catch (IOException ex) { throw new IllegalArgumentException("invalid message payload", ex); }
}
}5.7 Idempotent Execution Service
@Service
public class ConsumeIdempotentService {
private final JdbcTemplate jdbcTemplate;
private final PlatformTransactionManager transactionManager;
public ConsumeIdempotentService(JdbcTemplate jdbcTemplate,
PlatformTransactionManager transactionManager) {
this.jdbcTemplate = jdbcTemplate;
this.transactionManager = transactionManager;
}
public boolean executeOnce(String bizType, String eventId, Runnable runnable) {
TransactionTemplate template = new TransactionTemplate(transactionManager);
return Boolean.TRUE.equals(template.execute(status -> {
int inserted = jdbcTemplate.update("""
INSERT INTO mq_consume_idempotent
(consumer_group, biz_type, event_id, status)
VALUES (?, ?, ?, 0)
ON DUPLICATE KEY UPDATE event_id = event_id
""", "trade_order_paid_consumer", bizType, eventId);
if (inserted == 0) return false;
runnable.run();
jdbcTemplate.update("""
UPDATE mq_consume_idempotent
SET status = 1, result_code = ?
WHERE consumer_group = ? AND event_id = ?
""", "SUCCESS", "trade_order_paid_consumer", eventId);
return true;
}));
}
}6. High‑Concurrency Bottlenecks
Heavy local transaction : querying or updating many tables, calling remote services, complex rules, or sending SMS/email inside executeLocalTransaction inflates the critical section and creates back‑check storms.
Excessive UNKNOW : each unknown forces the broker to check back, increasing broker load, producer thread‑pool pressure, DB QPS, and alert noise.
Missing indexes on transaction table : without a unique index on transaction_id, check‑back queries become slow under load.
Thread‑pool mixing : using the same pool for normal business, producer sends, and check‑back leads to dead‑lock‑like cascades when back‑check threads are starved.
Recommendations:
Keep the local transaction minimal – only state progression, idempotent record, and transaction record.
Isolate producer send pool, check‑back pool, consumer pool, and DB connection pools.
Ensure unique indexes on transaction_id and event_id, plus a normal index on business_key.
7. Check‑Back Design Rules
7.1 Four Iron Rules for checkLocalTransaction
No side effects.
Idempotent – can be called repeatedly.
Result should be as deterministic as possible.
Execution time must be short.
Never modify business state, insert records, or call external services inside the check‑back.
7.2 Recommended Decision Order
Query the transaction‑state table.
If not found, query the main business state (e.g., order status).
If still ambiguous, look for compensation or retry logs.
Return UNKNOW only when the above cannot decide.
7.3 When to Return ROLLBACK_MESSAGE Directly
Parameter validation fails.
Business record does not exist.
State machine explicitly disallows progression.
Idempotent check confirms the event should not take effect.
7.4 When UNKNOW Is Legitimate
Transient DB read glitches.
Local transaction completed but result not yet persisted.
Brief window where both transaction record and business state may be recoverable.
8. Real‑World Case: Payment Success Chain
Payment platform notifies → verify signature → payment state → order state → RocketMQ Tx Msg (OrderPaidEvent) → stock confirm → coupon grant → points credit → fulfillment order creation → periodic reconciliation taskTransactional messages guarantee the link between "order is paid" and "event is emitted"; other capabilities (idempotent callbacks, state‑machine enforcement, downstream idempotency, alerts, compensation, T+1 reconciliation) must be added separately.
9. Common Pitfalls
Treating transactional messages as a distributed‑transaction silver bullet.
Embedding remote RPC calls inside the local transaction.
Neglecting consumer idempotency – RocketMQ still delivers at‑least‑once.
Storing check‑back state only in in‑memory maps; it disappears on restart or across instances.
Assuming "more reliable" without dedicated thread pools, indexes, idempotent tables, alerts, and compensation tasks.
10. Observability & Operations
10.1 Essential Metrics (sample table)
Metric | Meaning | Alert Suggestion
--------------------------|------------------------------------------|------------------------------
Transactional send success | Half‑send & final commit rate | Alert if 5‑min window drops
Local tx latency P95/P99 | executeLocalTransaction delay | Rising latency = overloaded tx region
UNKNOW proportion | % of ambiguous decisions | High % = upcoming check‑back pressure
Check‑back QPS | Broker’s back‑check frequency | Spike = fault precursor
Check‑back avg latency | DB query time for check‑back | High = DB or logic issue
Half‑message backlog | Pending decisions in broker | Growing = commit/rollback chain broken
Consume retry count | Downstream stability | Exceed threshold → investigate idempotency
Dead‑letter count | Final failures | Must have manual handling10.2 Structured Logging Fields
traceId transactionId eventId businessKey topic producerGroup consumerGroup retryTimesThese keys enable end‑to‑end tracing across send, check‑back, and consume.
10.3 Alert Coverage Beyond Broker
Often the first symptom appears in DB slow queries, thread‑pool queues, payment callback latency, or downstream retry spikes, not in broker metrics. Alerts must span broker, JVM, DB, and downstream services.
10.4 Failure‑Injection Playbooks
Producer crash after half message.
Commit ACK loss.
Transient DB read‑only mode.
High‑frequency duplicate payment callbacks.
Consumer timeout & duplicate delivery.
Downstream service outage causing retry buildup.
11. Deployment Stages
Single‑instance validation : verify commit, rollback, crash‑recovery, idempotent consumption.
Multi‑instance : ensure consistent check‑back results across producer group, shared transaction table, isolated check‑back pool, sufficient DB connections.
Multi‑topic / multi‑domain : separate topics per business domain (e.g., trade.order.paid, inventory.stock.confirmed) to isolate traffic, consumer groups, alerts, and dead‑letter handling.
Cross‑region / multi‑active : transactional messages only guarantee consistency within a single business unit; global strong consistency still requires compensation, reconciliation, and async replication.
12. Performance Tuning Priorities
Shrink local transaction critical section – keep only state update, idempotent record, transaction record.
Reduce UNKNOW ratio – make failures explicit, avoid swallowing exceptions.
DB & index optimization – unique indexes on transaction_id and event_id, hot‑key distribution, eliminate slow SQL.
Thread‑pool & batch tuning – only after semantics are correct; otherwise you only amplify errors.
13. Suitability Checklist
Business state change is deterministic (e.g., order paid).
Downstream can tolerate eventual consistency.
Need a tight coupling between local state and event emission.
Not suitable for strong cross‑service transactions, long‑running manual approvals, or scenarios requiring immediate confirmation from all downstream services.
When multiple MQs, rich audit, or platform‑wide fan‑out is needed, consider Outbox or CDC instead.
14. Pre‑Production Checklist
Verify executeLocalTransaction contains only minimal critical section.
Ensure checkLocalTransaction is side‑effect‑free, idempotent, and fast.
Do not return UNKNOW for all exceptions.
Create required indexes on transaction and idempotent tables.
Isolate send, check‑back, and consumer thread pools.
Validate idempotent handling under duplicate notifications and duplicate consumption.
Set up metrics for send success rate, UNKNOW proportion, check‑back QPS, half‑message backlog, consumer retry, dead‑letter counts.
Implement structured logs with traceId/transactionId/eventId.
Prepare manual dead‑letter handling or automated compensation.
Run failure‑injection drills: producer crash, ACK loss, DB glitch, downstream outage.
15. Conclusion
RocketMQ transactional messages close the most common consistency gap – the link between a local state transition and the reliable emission of an event. They are not a universal distributed‑transaction solution; they must be combined with domain state machines, idempotent consumption, compensation, observability, and rigorous operational practices to achieve production‑grade eventual consistency.
References
Apache RocketMQ official documentation: Transactional Message
Apache RocketMQ transactional message design description
Apache RocketMQ source code repository
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Cloud Architecture
Focuses on cloud‑native and distributed architecture engineering, sharing practical solutions and lessons learned. Covers microservice governance, Kubernetes, observability, and stability engineering to help your systems run stable, fast, and cost‑effectively.
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.
