分布式事务的典型场景与挑战
微服务架构下,一个业务操作往往跨越多个服务,每个服务维护自己的数据库实例。例如电商下单流程涉及:订单服务创建订单、库存服务扣减库存、账户服务扣减余额。三步操作要么全部成功,要么全部回滚。但各服务数据库独立,无法使用本地事务保证一致性——这就是分布式事务问题。
常见的分布式事务方案有四种:2PC(两阶段提交)、TCC(Try-Confirm-Cancel)、Saga(长事务编排)、本地消息表+最终一致性。没有银弹,方案选择取决于业务对一致性的容忍度和系统复杂度承受能力。
2PC两阶段提交与XA事务
2PC是最严格的分布式事务方案,通过协调者(Coordinator)统一控制所有参与者的提交或回滚。第一阶段(Prepare)各参与者执行事务但不提交,将结果告知协调者;第二阶段(Commit/Abort)协调者根据第一阶段结果决定全局提交或回滚。XA协议是2PC在数据库层面的标准实现,MySQL、PostgreSQL均支持。
# Spring Boot + Atomikos XA事务配置
# pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
# application.yml
spring:
jta:
atomikos:
datasource:
order-db:
unique-resource-name: orderDataSource
xa-properties:
url: jdbc:mysql://order-db:3306/order
user: root
password: xxx
pool-size: 10
account-db:
unique-resource-name: accountDataSource
xa-properties:
url: jdbc:mysql://account-db:3306/account
user: root
password: xxx
pool-size: 10
# OrderService.java
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private AccountClient accountClient;
@Autowired
private InventoryClient inventoryClient;
@Transactional // JTA全局事务
public void createOrder(Order order) {
orderMapper.insert(order);
accountClient.deduct(order.getUserId(), order.getAmount());
inventoryClient.deduct(order.getProductId(), order.getQuantity());
}
}
2PC的缺点在于同步阻塞——Prepare阶段所有参与者持有锁不释放,直到第二阶段完成。如果协调者宕机,参与者会长时间阻塞等待,导致资源锁定。因此2PC只适合事务参与者少、执行时间短的场景,不推荐用于跨多个服务的长流程。
TCC模式与Seata AT实现
TCC将业务拆为三个阶段:Try(预留资源)、Confirm(确认提交)、Cancel(取消预留)。与2PC不同,TCC的Try阶段不持有数据库锁,而是通过业务逻辑预留资源——比如扣减库存时先冻结而非真正扣减。这降低了对数据库的锁定时间,但要求每个业务接口都实现三套方法,开发成本高。
Seata的AT模式在TCC基础上做了自动化——框架自动生成回滚日志(undo_log),业务代码只需要写正常分支逻辑。一阶段执行业务SQL并记录前镜像(before image),二阶段根据全局事务结果自动提交或用前镜像做反向补偿。
// Seata AT模式配置
// 1. 引入依赖
// implementation 'io.seata:seata-spring-boot-starter:2.0.0'
// 2. 在业务入口标注全局事务
@GlobalTransactional(timeoutMills = 60000, name = "create-order")
public void createOrder(OrderDTO dto) {
// 以下每个远程调用自动纳入Seata全局事务
orderService.create(dto); // 一阶段:插入订单记录+记录undo_log
inventoryService.deduct(dto.getProductId(), dto.getQty()); // 冻结库存
accountService.deduct(dto.getUserId(), dto.getAmount()); // 冻结余额
// 全部成功→自动提交;任一失败→Seata根据undo_log自动补偿回滚
}
// 3. 每个微服务数据库建undo_log表
// CREATE TABLE undo_log (
// id BIGINT PRIMARY KEY AUTO_INCREMENT,
// branch_id BIGINT NOT NULL,
// xid VARCHAR(100) NOT NULL,
// context VARCHAR(128) NOT NULL,
// rollback_info LONGBLOB NOT NULL,
// log_status INT NOT NULL,
// log_created DATETIME NOT NULL,
// log_modified DATETIME NOT NULL,
// UNIQUE KEY ux_undo_log (xid, branch_id)
// );
Saga模式与长事务编排
Saga将长事务拆为一串本地事务,每个本地事务提交后触发下一个步骤。如果某步骤失败,反向执行已完成步骤的补偿操作。相比TCC,Saga没有全局锁,每个步骤完成后立即释放资源,吞吐更高但一致性更弱——中间状态可被外部观察到。
Saga有两种编排方式:集中式(Orchestration)由一个协调器控制流程,各参与者只实现正向和补偿操作;事件驱动(Choreography)通过消息队列串联,每个步骤完成后发布事件触发下一环节。集中式逻辑清晰易调试,适合流程固定且步骤不超过10个的场景。
// Seata Saga状态机定义(JSON DSL)
// resources/statelang/create_order.json
{
"Name": "createOrder",
"Comment": "创建订单Saga流程",
"StartState": "CreateOrder",
"States": {
"CreateOrder": {
"Type": "ServiceTask",
"ServiceName": "orderService",
"ServiceMethod": "create",
"CompensateState": "CancelOrder",
"Next": "DeductInventory",
"Input": ["$.orderDTO"],
"Output": { "orderId": "$.orderId" }
},
"DeductInventory": {
"Type": "ServiceTask",
"ServiceName": "inventoryService",
"ServiceMethod": "deduct",
"CompensateState": "CompensateInventory",
"Next": "DeductAccount",
"Input": ["$.orderDTO.productId", "$.orderDTO.qty"]
},
"DeductAccount": {
"Type": "ServiceTask",
"ServiceName": "accountService",
"ServiceMethod": "deduct",
"CompensateState": "CompensateAccount",
"Input": ["$.orderDTO.userId", "$.orderDTO.amount"],
"IsEnd": true
},
"CancelOrder": {
"Type": "ServiceTask",
"ServiceName": "orderService",
"ServiceMethod": "cancel",
"Input": ["$.orderId"],
"IsEnd": true
},
"CompensateInventory": {
"Type": "ServiceTask",
"ServiceName": "inventoryService",
"ServiceMethod": "compensate"
},
"CompensateAccount": {
"Type": "ServiceTask",
"ServiceName": "accountService",
"ServiceMethod": "compensate"
}
}
}
本地消息表实现最终一致性
本地消息表是最轻量的分布式事务方案——不需要引入任何事务框架,利用本地事务+消息重试保证最终一致性。核心思路:业务操作和消息记录在同一个本地事务中写入,后台定时任务扫描消息表,将未发送的消息投递到MQ,消费者处理后更新消息状态。
// 本地消息表方案实现
// 1. 消息表结构
// CREATE TABLE outbox_message (
// id BIGINT PRIMARY KEY AUTO_INCREMENT,
// biz_type VARCHAR(64) NOT NULL,
// biz_id VARCHAR(128) NOT NULL,
// content JSON NOT NULL,
// status TINYINT DEFAULT 0, -- 0待发送 1已发送 2失败
// retry_count INT DEFAULT 0,
// max_retry INT DEFAULT 5,
// created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
// INDEX idx_status_created (status, created_at)
// );
@Service
@Slf4j
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private OutboxMessageMapper outboxMapper;
@Transactional
public void createOrder(Order order) {
// 业务操作
orderMapper.insert(order);
// 同一事务写入消息表
OutboxMessage msg = new OutboxMessage();
msg.setBizType("ORDER_CREATED");
msg.setBizId(order.getId().toString());
msg.setContent(JSON.toJSONString(order));
msg.setStatus(0);
outboxMapper.insert(msg);
// 本地事务提交——业务和消息要么同时成功,要么同时回滚
}
}
// 消息投递定时任务
@Component
@Slf4j
public class OutboxMessageSender {
@Autowired
private OutboxMessageMapper outboxMapper;
@Autowired
private RocketMQTemplate mqTemplate;
@Scheduled(fixedDelay = 1000) // 每秒扫描一次
public void sendPendingMessages() {
List<OutboxMessage> messages = outboxMapper.selectPending(100);
for (OutboxMessage msg : messages) {
try {
mqTemplate.convertAndSend(msg.getBizType(), msg.getContent());
outboxMapper.updateStatus(msg.getId(), 1); // 标记已发送
} catch (Exception e) {
outboxMapper.incrementRetry(msg.getId());
if (msg.getRetryCount() + 1 >= msg.getMaxRetry()) {
outboxMapper.updateStatus(msg.getId(), 2); // 标记失败
log.error("Message {} exceeded max retry", msg.getId());
}
}
}
}
}
方案选型决策框架
四种方案的选择不是互斥的,同一系统中不同业务流程可以用不同方案。决策依据三个维度:一致性要求(强一致vs最终一致)、业务流程长度(3步以内vs5步以上)、基础设施复杂度容忍度。
2PC/XA:适合2-3个数据库之间的短事务,一致性要求极强,但吞吐低、阻塞风险高。TCC/Seata AT:适合3-5个服务的中等长度流程,需要Seata基础设施但开发侵入性适中。Saga:适合5步以上的长流程(如订单全流程包含支付、物流、退款),容忍短暂不一致。本地消息表:适合对端是异步消费者的场景(如下单后触发积分发放、短信通知),实现最简单但只能保证最终一致性。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/springboot-fen-bu-shi-shi-wu-yi-zhi-xing-fang-an-xuan-xing/