欢迎光临
我们一直在努力

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

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

cover

一、微服务拆分后的数据一致性困局

单体应用中,一个数据库事务就能保证操作的原子性。但当系统按业务域拆分为订单、库存、账户、积分等独立微服务后,一个完整的业务操作跨越了多个数据库实例。例如电商下单流程:创建订单 → 扣减库存 → 扣除余额 → 增加积分,这四个步骤分布在四个服务中,任何一个失败都需要回滚前面的操作。

分布式事务的核心矛盾在于:强一致性需要分布式锁,而分布式锁严重损害系统的吞吐量和可用性。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 堆积量、本地消息表的积压量都必须纳入监控。分布式事务的失败往往不会立即暴露,需要主动发现。

赞(0)
未经允许不得转载:171主机测评 » Spring Cloud 微服务分布式事务:从 Seata AT 到最终一致性的工程落地
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址