Spring Cloud 微服务分布式事务:从 Seata AT 到最终一致性的工程落地

一、微服务拆分后的数据一致性困局
单体应用中,一个数据库事务就能保证操作的原子性。但当系统按业务域拆分为订单、库存、账户、积分等独立微服务后,一个完整的业务操作跨越了多个数据库实例。例如电商下单流程:创建订单 → 扣减库存 → 扣除余额 → 增加积分,这四个步骤分布在四个服务中,任何一个失败都需要回滚前面的操作。
分布式事务的核心矛盾在于:强一致性需要分布式锁,而分布式锁严重损害系统的吞吐量和可用性。CAP 定理告诉我们,在网络分区发生时,一致性和可用性不可兼得。在微服务架构下,网络分区是常态而非异常,因此追求强一致性的两阶段提交(2PC)方案在互联网场景中鲜有成功案例。
工程实践中的选择是:对一致性要求极高的场景(如资金转账)采用 TCC 模式,对大多数业务场景采用最终一致性方案。本文将深入剖析 Seata AT 模式的实现原理,并给出从 AT 到 TCC 再到最终一致性的完整落地路径。
二、分布式事务的核心机制与 Seata AT 原理
2.1 分布式事务协议对比
| 2PC | 强一致 | 低 | 低 | 传统数据库跨库事务 |
| TCC | 强一致 | 中 | 高 | 资金交易、库存扣减 |
| Seata AT | 最终一致 | 高 | 低 | 通用业务场景 |
| Saga | 最终一致 | 高 | 中 | 长流程编排 |
| 本地消息表 | 最终一致 | 高 | 中 | 异步解耦场景 |
2.2 Seata AT 模式的两阶段提交原理
Seata AT 模式是对 2PC 的业务层优化。一阶段直接提交本地事务,同时记录回滚日志(Undo Log);二阶段根据全局事务结果,提交时异步清理 Undo Log,回滚时根据 Undo Log 反向补偿。
sequenceDiagram
participant TM as 事务管理器(TM)
participant TC as 事务协调器(TC)
participant RM1 as 资源管理器-订单(RM)
participant RM2 as 资源管理器-库存(RM)
participant DB1 as 订单库
participant DB2 as 库存库
TM->>TC: 开启全局事务(XID)
TC–>>TM: 返回 XID
rect rgb(240, 248, 255)
Note over TM,DB2: 一阶段:业务SQL + Undo Log
TM->>RM1: 执行订单创建(XID)
RM1->>DB1: 解析SQL生成前镜像
RM1->>DB1: 执行业务SQL
RM1->>DB1: 生成后镜像 + 写入Undo Log
RM1->>DB1: 提交本地事务
RM1–>>TC: 一阶段完成
TM->>RM2: 执行库存扣减(XID)
RM2->>DB2: 解析SQL生成前镜像
RM2->>DB2: 执行业务SQL
RM2->>DB2: 生成后镜像 + 写入Undo Log
RM2->>DB2: 提交本地事务
RM2–>>TC: 一阶段完成
end
alt 全局提交
TC->>RM1: 异步清理 Undo Log
TC->>RM2: 异步清理 Undo Log
else 全局回滚
TC->>RM1: 读取 Undo Log 反向补偿
RM1->>DB1: 执行反向 SQL
TC->>RM2: 读取 Undo Log 反向补偿
RM2->>DB2: 执行反向 SQL
end
AT 模式的关键优势在于:一阶段直接提交本地事务,不持有分布式锁,因此性能接近原生本地事务。代价是回滚时需要依赖 Undo Log 做反向补偿,如果补偿期间数据已被其他事务修改,可能出现脏回滚。
2.3 脏回滚的检测与防护
Seata 通过前后镜像对比检测脏回滚:如果 Undo Log 中记录的后镜像与当前数据库中的数据不一致,说明数据已被其他事务修改,此时回滚会覆盖他人的修改。Seata 的策略是抛出异常,由人工介入处理。
flowchart TD
A[收到回滚指令] –> B[读取 Undo Log]
B –> C{后镜像 == 当前数据?}
C –>|一致| D[执行反向 SQL 回滚]
C –>|不一致| E{是否为脏回滚?}
E –>|是| F[抛出异常,人工介入]
E –>|否,仅非关键字段变化| G[更新 Undo Log 后重试回滚]
D –> H[删除 Undo Log]
G –> H
三、生产级代码实现与最佳实践
3.1 Seata AT 模式集成配置
# application-seata.yml
seata:
enabled: true
application-id: order-service
tx-service-group: order-tx-group
service:
vgroup-mapping:
order-tx-group: default
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: seata
group: SEATA_GROUP
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: seata
# Undo Log 序列化配置
client:
undo:
data-serialization: jackson
log-serialization: jackson
# 仅记录被修改字段的 Undo Log,减少存储开销
only-care-update-columns: true
3.2 订单服务的分布式事务入口
@Service
public class OrderServiceImpl implements OrderService {
private final OrderMapper orderMapper;
private final InventoryClient inventoryClient;
private final AccountClient accountClient;
private final PointsClient pointsClient;
private final RocketMQTemplate rocketMQTemplate;
/**
* 下单流程:Seata AT 模式管理全局事务
* @GlobalTransactional 注解由 Seata 拦截器处理,
* 自动注册全局事务、传播 XID、协调分支事务
*/
@Override
@GlobalTransactional(timeoutMills = 30000, name = "create-order")
public OrderResult createOrder(CreateOrderRequest request) {
// 1. 创建订单(本地事务,Seata RM 自动拦截)
Order order = Order.builder()
.orderId(IdWorker.getIdStr())
.userId(request.getUserId())
.productId(request.getProductId())
.quantity(request.getQuantity())
.totalAmount(request.getTotalAmount())
.status(OrderStatus.CREATED)
.build();
orderMapper.insert(order);
// 2. 扣减库存(远程调用,XID 自动传播)
InventoryResult invResult = inventoryClient.deduct(
request.getProductId(), request.getQuantity());
if (!invResult.isSuccess()) {
throw new BusinessException("库存不足,扣减失败");
}
// 3. 扣除账户余额
AccountResult accResult = accountClient.debit(
request.getUserId(), request.getTotalAmount());
if (!accResult.isSuccess()) {
throw new BusinessException("余额不足,扣款失败");
}
// 4. 增加积分(非核心操作,失败不影响主流程)
try {
pointsClient.earn(request.getUserId(),
request.getTotalAmount().intValue());
} catch (Exception e) {
// 积分失败记录日志,通过 MQ 异步补偿
log.warn("积分发放失败,进入异步补偿: orderId={}",
order.getOrderId(), e);
rocketMQTemplate.convertAndSend(
"points-compensation-topic",
new PointsCompensationEvent(
order.getOrderId(),
request.getUserId(),
request.getTotalAmount().intValue())
);
}
order.setStatus(OrderStatus.PAID);
orderMapper.updateStatus(order);
return OrderResult.success(order.getOrderId());
}
}
3.3 从 AT 到最终一致性:本地消息表方案
对于积分这类非核心操作,使用 Seata AT 的强一致性保障是过度设计。本地消息表方案更合适:
@Service
public class LocalMessageTableService {
private final JdbcTemplate jdbcTemplate;
private final RocketMQTemplate rocketMQTemplate;
/**
* 在同一个本地事务中完成业务操作和消息记录
* 保证业务操作与消息记录的原子性
*/
@Transactional
public void executeWithMessage(BusinessAction action,
String topic, Object payload) {
// 1. 执行业务操作
action.execute();
// 2. 写入本地消息表(与业务操作在同一事务中)
String messageId = IdWorker.getIdStr();
jdbcTemplate.update(
"INSERT INTO outbox_messages " +
"(message_id, topic, payload, status, created_at) " +
"VALUES (?, ?, ?, 'PENDING', NOW())",
messageId, topic, JsonUtils.toJson(payload));
}
/**
* 定时任务:扫描未发送的消息并投递到 MQ
* 幂等投递:MQ 生产者开启事务消息,确保不丢不重
*/
@Scheduled(fixedRate = 5000)
public void scanAndPublish() {
List<Map<String, Object>> messages = jdbcTemplate.queryForList(
"SELECT * FROM outbox_messages " +
"WHERE status = 'PENDING' AND retry_count < 5 " +
"ORDER BY created_at ASC LIMIT 100");
for (Map<String, Object> msg : messages) {
try {
// 发送事务消息,确保 MQ 端不丢
rocketMQTemplate.sendMessageInTransaction(
(String) msg.get("topic"),
MessageBuilder.withPayload(msg.get("payload"))
.setHeader("messageId", msg.get("message_id"))
.build(),
msg.get("message_id"));
jdbcTemplate.update(
"UPDATE outbox_messages SET status = 'SENT' " +
"WHERE message_id = ?",
msg.get("message_id"));
} catch (Exception e) {
// 发送失败,增加重试计数
jdbcTemplate.update(
"UPDATE outbox_messages " +
"SET retry_count = retry_count + 1, " +
" next_retry_at = DATE_ADD(NOW(), " +
" INTERVAL POWER(2, retry_count) SECOND) " +
"WHERE message_id = ?",
msg.get("message_id"));
}
}
}
}
四、架构权衡与选型决策
4.1 Seata AT 的性能代价
AT 模式一阶段需要解析 SQL 生成前后镜像,这个过程的性能开销约为原生 SQL 执行时间的 1.2-1.5 倍。Undo Log 的写入也增加了数据库的存储和 I/O 压力。在高并发场景下,Undo Log 表可能成为热点,需要定期清理。
4.2 全局锁的竞争问题
Seata AT 在一阶段提交前会申请全局锁,防止其他全局事务同时修改同一行数据。如果两个全局事务同时操作同一行,后申请的事务会等待全局锁释放,超时后回滚。这意味着 AT 模式在热点数据上的并发性能会显著下降。
4.3 TC 单点风险
Seata TC(事务协调器)是全局事务的大脑。如果 TC 宕机,正在进行中的全局事务将无法完成回滚,Undo Log 会残留在数据库中。TC 必须部署为集群模式,并使用数据库存储事务日志以支持故障恢复。
4.4 方案选型决策树
flowchart TD
A[分布式事务需求] –> B{是否涉及资金?}
B –>|是| C{是否允许短暂不一致?}
C –>|否| D[TCC 模式:强一致保障]
C –>|是| E[Seata AT + 异步补偿]
B –>|否| F{是否需要实时感知结果?}
F –>|是| G[Seata AT 模式]
F –>|否| H[本地消息表 + 最终一致性]
五、总结
分布式事务没有银弹,每种方案都是在一致性、性能和复杂度之间做取舍。落地路线建议如下:
第一,按业务场景分级治理。资金类操作用 TCC,核心业务链路用 Seata AT,非核心操作用本地消息表。不要一刀切地选择同一种方案。
第二,优先减少分布式事务的发生。通过领域驱动设计(DDD)的聚合根划分,将强一致的操作收敛到同一个微服务内。跨服务调用越少,分布式事务越少。
第三,幂等是最终一致性的前提。无论是 MQ 重试还是定时补偿,消费端必须保证幂等。推荐使用唯一业务 ID + 状态机双重校验。
第四,监控与告警不可或缺。Seata TC 的全局事务成功率、Undo Log 堆积量、本地消息表的积压量都必须纳入监控。分布式事务的失败往往不会立即暴露,需要主动发现。




