上周排查一个线上问题时,遇到了典型的跨库事务不一致案例。用户下单后,订单主表写入成功,但库存扣减却失败了。这种"半成功"状态在分布式系统中尤为棘手——两个数据库分别位于不同的物理服务器,无法通过本地事务保证原子性。
我遇到的具体场景是:电商系统中,订单服务使用MySQL的orders库,库存服务单独使用inventory库。当用户购买某件库存仅剩1件的商品时,如果这两个操作不在同一个事务中,就可能出现订单创建成功但库存未扣减的致命错误。
关键点:真正的生产环境往往采用微服务架构,不同服务有独立的数据库实例。这时传统的ACID事务无法直接跨服务生效。
在单数据库环境下,事务的ACID特性由数据库引擎保证。以MySQL为例:
sql复制START TRANSACTION;
INSERT INTO orders(...); -- 操作1
UPDATE inventory SET stock=stock-1 WHERE product_id=123; -- 操作2
COMMIT;
当这两个表在同一个数据库实例时,任何一条语句失败都会导致整个事务回滚。但一旦分属不同数据库,这个机制就失效了。
实践中解决跨库事务主要有两种思路:
强一致性方案:如XA协议、Seata等分布式事务框架
最终一致性方案:如消息队列+定时任务补偿
下表对比两种方案的特性:
| 维度 | 强一致性 | 最终一致性 |
|---|---|---|
| 数据时效性 | 实时一致 | 短暂延迟 |
| 系统吞吐量 | 较低(约500TPS) | 较高(可达万级TPS) |
| 实现复杂度 | 高(需协调器) | 中(需设计补偿逻辑) |
| 适用场景 | 金融转账、库存冻结 | 订单创建、日志记录 |
根据我们的业务特点(电商订单),选择最终一致性方案更合适。具体决策依据:
核心流程如下图所示(文字描述):
订单服务:
库存服务:
定时任务:
关键代码示例(订单服务部分):
java复制@Transactional
public void createOrder(OrderDTO order) {
// 1. 保存订单(初始状态为PROCESSING)
order.setStatus(OrderStatus.PROCESSING);
orderMapper.insert(order);
// 2. 发送库存消息(保证与订单事务同步)
inventoryProducer.sendDeductMessage(
new InventoryDeductDTO(order.getProductId(), order.getQuantity()));
// 3. 事务提交时消息才会真正投递
}
在实际编码中,这几个边界条件必须处理:
消息发送失败:
库存不足:
重复消费:
我们采用RabbitMQ的confirm模式增强可靠性:
java复制// 发送消息时设置确认回调
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if (!ack) {
log.error("消息投递失败: {}", cause);
// 触发告警并记录到异常表
}
});
同时配置生产端的重试策略:
yaml复制spring:
rabbitmq:
template:
retry:
enabled: true
initial-interval: 1000ms
multiplier: 2
max-attempts: 3
补偿任务需要特别注意:
sql复制UPDATE orders
SET status = 'FAILED'
WHERE id = ? AND status = 'PROCESSING'
订单状态分布看板:
消息堆积告警:
关键日志必须包含:
示例日志格式:
code复制[订单创建] orderId=20230815123456, status=PROCESSING, cost=45ms
[库存扣减] orderId=20230815123456, productId=789, result=SUCCESS
这套方案后续可以进一步优化:
不过在实际项目中,我建议先从简单的消息队列方案开始,等真正遇到规模瓶颈再考虑更复杂的方案。毕竟,没有完美的架构,只有适合当前业务阶段的架构。