微服务分布式事务为什么难做
单体应用中,事务由数据库的ACID机制保证,一个@Transactional注解就能解决数据一致性问题。拆分成微服务后,一个业务操作可能横跨订单、库存、支付三个服务,每个服务有自己的数据库实例。跨库事务不能依赖数据库本地事务机制,必须引入分布式事务框架来协调多个参与者的提交和回滚。
分布式事务的核心难题是部分失败:订单服务成功了,库存服务回滚了,支付服务超时了——系统处于不一致的中间状态,必须有机制能够自动恢复到一致状态。Seata是阿里开源的分布式事务框架,提供AT、TCC、SAGA、XA四种模式,其中AT模式对业务代码侵入最小,适合大多数Spring Boot微服务场景。
Seata AT模式工作原理
AT模式的全称是Automatic Transaction,核心思路是两阶段提交的自动版:
一阶段:拦截业务SQL,在执行前保存修改前的数据快照(before-image),执行后保存修改后的数据快照(after-image),生成行锁。快照和行锁保存在每个服务数据库的undo_log表中。
二阶段提交:删除undo_log和行锁。
二阶段回滚:读取undo_log中的before-image,反向补偿SQL将数据恢复到修改前的状态。
整个过程中,业务代码只需要添加一个@GlobalTransactional注解,不需要手动编写补偿逻辑。Seata通过数据源代理自动拦截SQL、记录快照、管理行锁。
Seata Server部署与配置
Seata Server(TC——Transaction Coordinator)是事务协调器,独立部署,负责管理全局事务的提交和回滚决策。
# docker-compose部署Seata Server
version: '3.8'
services:
seata-server:
image: seataio/seata-server:2.2.0
container_name: seata-server
ports:
- "8091:8091"
- "7091:7091"
environment:
- SEATA_IP=seata-server
- STORE_MODE=db
volumes:
- ./seata-config:/seata-server/resources
depends_on:
- mysql
networks:
- microservice-net
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: seata
volumes:
- ./sql:/docker-entrypoint-initdb.d
networks:
- microservice-net
networks:
microservice-net:
driver: bridge
# seata-config/application.yml——Seata Server配置
server:
port: 8091
seata:
store:
mode: db
db:
datasource: druid
db-type: mysql
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://mysql:3306/seata?rewriteBatchedStatements=true
username: root
password: root123
min-conn: 5
max-conn: 30
global-table: global_table
branch-table: branch_table
lock-table: lock_table
distributed-lock-table: distributed_lock
registry:
type: nacos
nacos:
server-addr: nacos:8848
namespace: seata
group: SEATA_GROUP
application: seata-server
config:
type: nacos
nacos:
server-addr: nacos:8848
namespace: seata
group: SEATA_GROUP
Spring Boot微服务集成Seata
每个参与分布式事务的微服务需要完成以下配置:引入Seata客户端依赖、配置数据源代理、创建undo_log表、注册到Seata Server。
<!-- pom.xml依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2023.0.1.2</version>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</version>
<version>2.2.0</version>
</dependency>
# application.yml——Seata客户端配置
seata:
enabled: true
application-id: order-service
tx-service-group: my-test-tx-group
service:
vgroup-mapping:
my-test-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
group: SEATA_GROUP
-- undo_log表DDL——每个业务数据库都需要创建
CREATE TABLE IF NOT EXISTS `undo_log` (
`branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT(11) NOT NULL COMMENT '0:normal, 1:defensed',
`log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB COMMENT ='AT transaction mode undo table';
订单-库存-支付三服务分布式事务实战
以电商下单场景为例,用户点击”确认支付”后,需要同时完成三个操作:创建订单、扣减库存、发起支付。任何一步失败都需要全部回滚。
// 订单服务——全局事务发起方
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private InventoryClient inventoryClient;
@Autowired
private PaymentClient paymentClient;
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public Order createOrder(OrderDTO orderDTO) {
// 1. 创建订单(本地事务)
Order order = new Order();
order.setUserId(orderDTO.getUserId());
order.setProductId(orderDTO.getProductId());
order.setQuantity(orderDTO.getQuantity());
order.setAmount(orderDTO.getAmount());
order.setStatus(OrderStatus.CREATED);
orderMapper.insert(order);
// 2. 扣减库存(远程调用,Seata自动纳入全局事务)
inventoryClient.deduct(
order.getProductId(),
order.getQuantity()
);
// 3. 发起支付(远程调用)
paymentClient.pay(
order.getId(),
order.getAmount()
);
// 4. 更新订单状态
order.setStatus(OrderStatus.PAID);
orderMapper.updateById(order);
return order;
}
}
// 库存服务——事务分支
@Service
public class InventoryService {
@Autowired
private InventoryMapper inventoryMapper;
@Transactional(rollbackFor = Exception.class)
public void deduct(Long productId, Integer quantity) {
Inventory inventory = inventoryMapper.selectByProductId(productId);
if (inventory == null) {
throw new BusinessException("Product not found");
}
if (inventory.getStock() < quantity) {
throw new BusinessException("Insufficient stock");
}
// Seata AT模式会自动记录before/after image
inventory.setStock(inventory.getStock() - quantity);
inventoryMapper.updateById(inventory);
}
}
// 支付服务——事务分支
@Service
public class PaymentService {
@Autowired
private PaymentMapper paymentMapper;
@Autowired
private AccountClient accountClient;
@Transactional(rollbackFor = Exception.class)
public void pay(Long orderId, BigDecimal amount) {
// 创建支付记录
Payment payment = new Payment();
payment.setOrderId(orderId);
payment.setAmount(amount);
payment.setStatus(PaymentStatus.PROCESSING);
paymentMapper.insert(payment);
// 调用账户扣款
accountClient.deductAmount(amount);
payment.setStatus(PaymentStatus.SUCCESS);
paymentMapper.updateById(payment);
}
}
全局锁与写隔离
AT模式下,Seata通过全局锁机制避免脏写问题。当全局事务T1正在修改某行数据时,全局锁会阻止其他全局事务T2同时修改同一行。但本地事务不受全局锁约束——如果一个非Seata管理的本地事务同时修改该行,就会出现脏写。
解决方案是在代理数据源层面拦截写操作,或者在数据库层面添加for update行锁。Seata 2.x已经支持通过配置强制全局锁检查:
# 强制全局锁检查配置
seata:
client:
rm:
lock-select-for-update: true
report-retry-count: 5
report-success-enable: true
async-commit-buffer-limit: 10000
Seata TCC模式处理复杂业务补偿
AT模式适合简单的CRUD场景,但如果业务操作不是简单的SQL增删改(如调用第三方支付、发送消息等),AT模式无法自动生成补偿SQL,需要使用TCC模式手动定义Try/Confirm/Cancel三个阶段的逻辑。
// TCC模式——自定义三阶段逻辑
@LocalTCC
public interface PaymentTccService {
@TwoPhaseBusinessAction(
name = "paymentTcc",
commitMethod = "confirm",
rollbackMethod = "cancel"
)
boolean tryPay(
@BusinessActionContextParameter(paramName = "orderId") Long orderId,
@BusinessActionContextParameter(paramName = "amount") BigDecimal amount
);
boolean confirm(BusinessActionContext context);
boolean cancel(BusinessActionContext context);
}
@Service
public class PaymentTccServiceImpl implements PaymentTccService {
@Autowired
private PaymentMapper paymentMapper;
@Autowired
private AccountClient accountClient;
@Override
public boolean tryPay(Long orderId, BigDecimal amount) {
// Try阶段:冻结资金,创建待确认支付记录
Payment payment = new Payment();
payment.setOrderId(orderId);
payment.setAmount(amount);
payment.setStatus(PaymentStatus.FROZEN); // 冻结状态
paymentMapper.insert(payment);
// 冻结账户余额
accountClient.freeze(amount);
return true;
}
@Override
public boolean confirm(BusinessActionContext context) {
// Confirm阶段:扣减冻结资金,支付成功
Long orderId = context.getActionContext("orderId", Long.class);
Payment payment = paymentMapper.selectByOrderId(orderId);
payment.setStatus(PaymentStatus.SUCCESS);
paymentMapper.updateById(payment);
// 扣减冻结资金
accountClient.deductFrozen(payment.getAmount());
return true;
}
@Override
public boolean cancel(BusinessActionContext context) {
// Cancel阶段:解冻资金,支付取消
Long orderId = context.getActionContext("orderId", Long.class);
Payment payment = paymentMapper.selectByOrderId(orderId);
if (payment != null) {
payment.setStatus(PaymentStatus.CANCELLED);
paymentMapper.updateById(payment);
// 解冻账户余额
accountClient.unfreeze(payment.getAmount());
}
return true;
}
}
分布式事务监控与问题诊断
Seata事务在生产环境中最常见的问题是全局事务超时和回滚失败。需要建立监控和告警体系:
# Prometheus监控Seata指标
# 全局事务成功率
sum(rate(seata_transaction_total{status="committed"}[5m]))
/
sum(rate(seata_transaction_total[5m]))
# 全局事务平均耗时
histogram_quantile(0.95,
sum(rate(seata_transaction_duration_seconds_bucket[5m])) by (le)
)
# 回滚失败告警
sum(increase(seata_transaction_total{status="rollback_failed"}[5m])) > 0
| 常见问题 | 原因 | 排查方向 |
|---|---|---|
| 全局事务超时 | 下游服务响应慢 | 检查分支事务执行日志,调整超时时间 |
| undo_log残留 | 回滚时网络中断 | 手动清理过期undo_log,检查TC日志 |
| 全局锁等待超时 | 长事务占用锁 | 缩短事务范围,添加锁重试配置 |
| 回滚补偿失败 | 数据已被其他事务修改 | 检查脏写场景,启用lock-select-for-update |
Seata AT模式通过自动化SQL拦截和快照回滚,把分布式事务的复杂度从业务代码中剥离出来。但自动化不代表零运维——全局锁冲突、回滚补偿失败、undo_log膨胀这些问题需要配套的监控告警和应急处理流程。TCC模式虽然侵入性更强,但在涉及外部系统交互和复杂业务补偿的场景下仍然是必要的。AT和TCC可以在同一个系统中混合使用,根据每个事务分支的特征选择最合适的模式。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/springboot-wei-fu-wu-fen-bu-shi-shi-wu-seata-shi-zhan-at-mo/