副标题:半消息、本地事务、消息回查,三步走保证最终一致性!🎯
🎬 开场:分布式事务的噩梦
经典场景:电商下单 🛒:
用户下单购买商品,需要:
1. 创建订单(订单服务)
2. 扣减库存(库存服务)
3. 扣减余额(账户服务)
4. 发送优惠券(营销服务)
问题:如何保证这些操作要么全部成功,要么全部失败?
传统方案的问题:
| 方案 | 问题 |
|---|---|
| 分布式事务(2PC/3PC) | 性能差,阻塞,不适合高并发 ❌ |
| TCC | 代码侵入性强,复杂度高 ❌ |
| Saga | 需要补偿逻辑,实现复杂 ❌ |
RocketMQ事务消息:
- 性能高 ✅
- 实现简单 ✅
- 最终一致性 ✅
📚 什么是事务消息?
核心概念
事务消息(Transactional Message):
保证本地事务和消息发送的原子性
要么:本地事务成功 + 消息发送成功
要么:本地事务失败 + 消息不发送
避免了:
❌ 本地事务成功,但消息发送失败
❌ 消息发送成功,但本地事务失败
生活例子 💰
转账场景:
普通转账(可能出问题):
1. 你的账户扣款 -100元 ✅
2. 发送"转账成功"通知
3. 对方账户到账 +100元 ❌ 失败了!
结果:你的钱扣了,对方没收到!💔
事务消息转账(安全):
1. 你的账户扣款 -100元 ✅
2. 如果扣款成功,才发送"转账成功"消息
3. 对方收到消息后,账户到账 +100元 ✅
结果:要么都成功,要么都失败!😊
🔑 RocketMQ事务消息原理
三个核心概念
1. 半消息(Half Message)
半消息:
- 已经发送到Broker
- 但消费者看不到
- 处于"待确认"状态
就像快递已经到了快递柜,但还没通知你取件
2. 消息状态
COMMIT_MESSAGE: 提交消息(消费者可以消费)
ROLLBACK_MESSAGE: 回滚消息(删除消息)
UNKNOW: 状态未知(需要回查)
3. 消息回查(Message Check)
如果Broker长时间收不到确认
就会主动询问生产者:
"这个事务到底成功没有?"
生产者查询本地事务状态并回复
完整流程图
生产者 RocketMQ Broker 消费者
│ │ │
│ ① 发送半消息 │ │
├──────────────────────→ │ │
│ │ 存储半消息 │
│ │ (消费者看不到) │
│ ② 返回发送成功 │ │
│ ←──────────────────────┤ │
│ │ │
│ ③ 执行本地事务 │ │
│ (扣款/创建订单等) │ │
│ │ │
│ ④ 提交/回滚消息 │ │
├──────────────────────→ │ │
│ │ │
│ │ 如果是COMMIT: │
│ │ 消息变为可见 │
│ ├────────────────────→ │
│ │ │ ⑤ 消费消息
│ │ │ (加库存等)
│ │ │
│ ⑥ 如果超时未收到确认 │ │
│ ←──────────────────────┤ 回查本地事务状态 │
│ 查询事务状态 │ │
├──────────────────────→ │ │
│ 返回COMMIT/ROLLBACK │ │
│ │ │
💻 代码实现
1. 生产者发送事务消息
@Service
public class OrderTransactionProducer {
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Autowired
private OrderService orderService;
/**
* 发送事务消息
*/
public void sendTransactionMessage(OrderDTO orderDTO) {
// 构建消息
Message<OrderDTO> message = MessageBuilder
.withPayload(orderDTO)
.build();
// 发送事务消息
TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction(
"order-topic", // topic
message, // 消息
orderDTO // 透传参数(传给本地事务)
);
log.info("事务消息发送结果: {}", result.getSendStatus());
}
}
2. 实现事务监听器
/**
* 事务消息监听器
*/
@RocketMQTransactionListener
public class OrderTransactionListener implements RocketMQLocalTransactionListener {
@Autowired
private OrderService orderService;
@Autowired
private TransactionLogService transactionLogService;
/**
* 执行本地事务
*
* 这个方法在发送半消息成功后执行
*/
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
// 1. 获取消息内容
OrderDTO orderDTO = (OrderDTO) arg;
String transactionId = msg.getHeaders().get("transactionId", String.class);
log.info("开始执行本地事务: transactionId={}", transactionId);
// 2. 记录事务日志(状态:处理中)
TransactionLog log = new TransactionLog();
log.setTransactionId(transactionId);
log.setStatus(TransactionStatus.PROCESSING);
log.setOrderData(JSON.toJSONString(orderDTO));
transactionLogService.save(log);
// 3. 执行本地事务
Order order = orderService.createOrder(orderDTO);
// 4. 更新事务日志(状态:成功)
log.setStatus(TransactionStatus.SUCCESS);
log.setOrderId(order.getId());
transactionLogService.update(log);
log.info("本地事务执行成功: orderId={}", order.getId());
// 5. 返回COMMIT,消息将被投递给消费者
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
log.error("本地事务执行失败", e);
// 更新事务日志(状态:失败)
// ...
// 6. 返回ROLLBACK,消息将被删除
return RocketMQLocalTransactionState.ROLLBACK;
}
}
/**
* 回查本地事务状态
*
* 如果Broker长时间没收到确认,会调用这个方法
*/
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
String transactionId = msg.getHeaders().get("transactionId", String.class);
log.info("回查本地事务状态: transactionId={}", transactionId);
// 1. 查询事务日志
TransactionLog log = transactionLogService.getByTransactionId(transactionId);
if (log == null) {
// 事务日志不存在,可能还没来得及执行
return RocketMQLocalTransactionState.UNKNOW;
}
// 2. 根据事务状态返回结果
switch (log.getStatus()) {
case SUCCESS:
// 本地事务成功,提交消息
return RocketMQLocalTransactionState.COMMIT;
case FAILED:
// 本地事务失败,回滚消息
return RocketMQLocalTransactionState.ROLLBACK;
case PROCESSING:
// 本地事务还在处理中,返回UNKNOW
// Broker会稍后再次回查
return RocketMQLocalTransactionState.UNKNOW;
default:
return RocketMQLocalTransactionState.UNKNOW;
}
}
}
3. 消费者处理消息
/**
* 订单消息消费者
*/
@Service
@RocketMQMessageListener(
topic = "order-topic",
consumerGroup = "order-consumer-group"
)
public class OrderConsumer implements RocketMQListener<OrderDTO> {
@Autowired
private InventoryService inventoryService;
@Autowired
private AccountService accountService;
@Override
public void onMessage(OrderDTO orderDTO) {
log.info("收到订单消息: {}", orderDTO);
try {
// 1. 扣减库存
inventoryService.deduct(
orderDTO.getProductId(),
orderDTO.getQuantity()
);
// 2. 扣减余额
accountService.deduct(
orderDTO.getUserId(),
orderDTO.getAmount()
);
// 3. 发送优惠券
couponService.send(orderDTO.getUserId());
log.info("订单处理成功: orderId={}", orderDTO.getOrderId());
} catch (Exception e) {
log.error("订单处理失败", e);
// 抛出异常,消息会重新消费
throw new RuntimeException("订单处理失败", e);
}
}
}
4. 事务日志表设计
CREATE TABLE transaction_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
transaction_id VARCHAR(64) UNIQUE NOT NULL, -- 事务ID
status VARCHAR(20) NOT NULL, -- 状态:PROCESSING/SUCCESS/FAILED
order_data TEXT, -- 订单数据
order_id BIGINT, -- 订单ID
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_transaction_id (transaction_id),
INDEX idx_status (status)
);
🔍 详细流程解析
正常流程(本地事务成功)
Step 1: 发送半消息
Producer → Broker: 我要发一条消息
Broker: 好的,我先存起来,但不让消费者看到
Step 2: 执行本地事务
Producer: 开始扣款...
Database: 扣款成功!
Step 3: 提交消息
Producer → Broker: 我的事务成功了,COMMIT这条消息
Broker: 好的,我把消息设为可见
Step 4: 消费消息
Consumer: 收到消息,开始加库存...
Database: 加库存成功!
结果:✅ 扣款成功 + 加库存成功
异常流程1(本地事务失败)
Step 1: 发送半消息
Producer → Broker: 我要发一条消息
Broker: 好的,我先存起来
Step 2: 执行本地事务
Producer: 开始扣款...
Database: ❌ 余额不足,扣款失败!
Step 3: 回滚消息
Producer → Broker: 我的事务失败了,ROLLBACK这条消息
Broker: 好的,我删除这条消息
结果:✅ 扣款失败 + 消息不发送(正确)
异常流程2(网络故障,需要回查)
Step 1: 发送半消息
Producer → Broker: 我要发一条消息
Broker: 好的,我先存起来
Step 2: 执行本地事务
Producer: 开始扣款...
Database: 扣款成功!
Step 3: 提交消息(网络故障)
Producer → Broker: 我的事务成功了,COMMIT...
网络: ❌ 断了!
Broker: 没收到确认消息...
Step 4: 等待超时(默认6秒)
Broker: 等了6秒还没收到,我主动问一下吧
Step 5: 回查事务状态
Broker → Producer: 事务ID=123的事务成功了吗?
Producer: 让我查一下事务日志...
Database: 事务ID=123的状态是SUCCESS
Producer → Broker: 成功了,COMMIT
Step 6: 提交消息
Broker: 好的,我把消息设为可见
结果:✅ 扣款成功 + 消息发送成功(通过回查恢复)
🎯 核心要点
1. 为什么需要事务日志表?
// ❌ 错误做法:没有事务日志
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
orderService.createOrder(orderDTO);
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
return RocketMQLocalTransactionState.ROLLBACK;
}
}
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
// ❌ 无法知道之前的事务是否成功
// 只能返回UNKNOW或瞎猜
return RocketMQLocalTransactionState.UNKNOW;
}
// ✅ 正确做法:使用事务日志
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
// 1. 记录事务日志
transactionLogService.save(transactionId, PROCESSING);
try {
// 2. 执行业务
orderService.createOrder(orderDTO);
// 3. 更新事务日志
transactionLogService.update(transactionId, SUCCESS);
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
transactionLogService.update(transactionId, FAILED);
return RocketMQLocalTransactionState.ROLLBACK;
}
}
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
// ✅ 查询事务日志,准确返回状态
TransactionLog log = transactionLogService.get(transactionId);
if (log.getStatus() == SUCCESS) {
return RocketMQLocalTransactionState.COMMIT;
} else {
return RocketMQLocalTransactionState.ROLLBACK;
}
}
2. 事务日志和业务操作必须在同一个事务
@Transactional
public void createOrder(OrderDTO orderDTO, String transactionId) {
// 1. 记录事务日志
TransactionLog log = new TransactionLog();
log.setTransactionId(transactionId);
log.setStatus(TransactionStatus.PROCESSING);
transactionLogMapper.insert(log); // 和业务在同一个事务
// 2. 创建订单
Order order = new Order();
order.setUserId(orderDTO.getUserId());
order.setAmount(orderDTO.getAmount());
orderMapper.insert(order);
// 3. 更新事务日志
log.setStatus(TransactionStatus.SUCCESS);
log.setOrderId(order.getId());
transactionLogMapper.update(log); // 和业务在同一个事务
// 要么都成功,要么都失败
}
3. 回查频率和超时设置
# application.yml
rocketmq:
producer:
# 回查间隔时间(默认60秒)
check-interval: 60000
# 最大回查次数(默认15次)
max-check-times: 15
回查时间线:
0s: 发送半消息
6s: 第1次回查
66s: 第2次回查
126s: 第3次回查
...
15次回查后还是UNKNOW → 删除消息
💡 最佳实践
1. 幂等性设计
/**
* 消费者必须保证幂等性
* 因为消息可能重复消费
*/
@Service
public class OrderConsumerIdempotent implements RocketMQListener<OrderDTO> {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
public void onMessage(OrderDTO orderDTO) {
String messageId = orderDTO.getMessageId();
String redisKey = "msg:processed:" + messageId;
// 检查是否已处理
Boolean isFirst = redisTemplate.opsForValue()
.setIfAbsent(redisKey, "1", 7, TimeUnit.DAYS);
if (isFirst != null && isFirst) {
// 首次处理
processOrder(orderDTO);
} else {
// 重复消息,跳过
log.warn("重复消息,跳过: {}", messageId);
}
}
}
2. 事务日志定期清理
/**
* 定期清理过期的事务日志
*/
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点
public void cleanExpiredLogs() {
// 删除30天前的已完成事务日志
transactionLogMapper.deleteExpired(30);
log.info("事务日志清理完成");
}
3. 监控和告警
/**
* 监控事务消息状态
*/
@Component
public class TransactionMessageMonitor {
@Scheduled(fixedDelay = 60000) // 每分钟
public void checkPendingTransactions() {
// 查询处理中的事务(超过5分钟)
List<TransactionLog> pending = transactionLogMapper
.selectPendingTransactions(5);
if (!pending.isEmpty()) {
// 发送告警
alertService.send(
"事务消息告警",
"有" + pending.size() + "个事务长时间未完成"
);
}
}
}
🆚 与其他方案对比
事务消息 vs 本地消息表
| 维度 | 事务消息 | 本地消息表 |
|---|---|---|
| 实现复杂度 | 简单 ⭐⭐ | 复杂 ⭐⭐⭐⭐ |
| 性能 | 高 ⭐⭐⭐⭐ | 中 ⭐⭐⭐ |
| 依赖 | 依赖MQ | 只依赖DB |
| 消息可靠性 | 高 | 高 |
| 适用场景 | RocketMQ环境 | 任何环境 |
本地消息表实现:
// 需要额外的定时任务扫描本地消息表
@Scheduled(fixedDelay = 1000)
public void scanAndSend() {
// 1. 查询待发送的消息
List<LocalMessage> messages = messageMapper.selectPending();
// 2. 逐条发送
for (LocalMessage message : messages) {
try {
mqProducer.send(message.getTopic(), message.getContent());
// 3. 标记为已发送
message.setStatus(MessageStatus.SENT);
messageMapper.update(message);
} catch (Exception e) {
log.error("消息发送失败", e);
}
}
}
// 比事务消息复杂很多!
🎯 面试高频问题
Q1:事务消息如何保证最终一致性?
A:
-
半消息机制:
- 消息先发送但不可见
- 本地事务执行完才提交
-
回查机制:
- 网络故障时通过回查恢复
- 最多回查15次
-
事务日志:
- 记录本地事务状态
- 回查时准确返回
Q2:如果本地事务成功但提交消息失败怎么办?
A:
通过回查机制解决:
1. 本地事务成功(已记录事务日志)
2. 提交消息时网络故障
3. Broker超时未收到确认
4. Broker发起回查
5. 查询事务日志,发现是SUCCESS
6. 返回COMMIT,消息投递成功
最终结果:✅ 一致
Q3:事务消息有什么限制?
A:
- 不支持延迟消息
- 不支持批量发送
- 单条消息大小限制4MB
- 回查次数限制15次
- 只支持RocketMQ
🎉 总结
核心要点 ✨
-
三个核心:
- 半消息(Half Message)
- 本地事务执行
- 消息回查(Check)
-
关键实现:
- 事务监听器
- 事务日志表
- 幂等性设计
-
适用场景:
- 订单创建
- 账户变更
- 积分变动
记忆口诀 📝
事务消息三步走,
半消息本地事务回查有。
第一步发半消息,
消息存储不可见。
第二步执行事务,
成功失败记日志。
第三步提交回滚,
网络故障回查救。
事务日志很重要,
回查状态全靠它。
幂等设计不能少,
重复消费要防好。
最终一致有保证,
分布式事务不用愁!
📚 参考资料
最后送你一句话:
"分布式事务没有银弹,选择适合的方案才是王道。"
愿你的事务消息稳定可靠,最终一致! 💳✨
表情包时间 🎭
没有事务消息前:
😱 本地事务成功了,消息发送失败了
💔 数据不一致了...
有了事务消息后:
😊 半消息保存好
✅ 本地事务执行
🎉 消息成功投递
开发者:
😎 终于不用担心数据不一致了!