从MySQL事务到Seata分布式事务:我的SpringBoot微服务是如何一步步引入AT模式并平滑切换的
从MySQL事务到Seata分布式事务:我的SpringBoot微服务是如何一步步引入AT模式并平滑切换的
去年我们的电商系统完成了从单体架构到微服务的拆分,订单、库存、账户三个核心模块被拆分成独立服务。当用户下单时,系统需要依次调用这三个服务完成创建订单、扣减库存和账户扣款操作。最初我们天真地认为,只要在每个服务的方法上添加Spring的@Transactional注解就能保证数据一致性——直到某天凌晨收到告警:一个用户下单后库存已扣减,但账户余额未变化,而订单状态却显示"已完成"。
1. 分布式事务问题的浮现与临时方案
1.1 第一个生产事故分析
那个引发事故的订单链路是这样的:
// 订单服务
@Transactional
public void createOrder(OrderDTO order) {
// 1. 本地创建订单记录
orderMapper.insert(order);
// 2. 远程调用库存服务
stockFeignClient.deduct(order.getProductId(), order.getQuantity());
// 3. 远程调用账户服务
accountFeignClient.debit(order.getUserId(), order.getAmount());
// 4. 更新订单状态
order.setStatus(OrderStatus.COMPLETED);
orderMapper.updateById(order);
}
当账户服务因网络抖动超时抛出异常时,我们发现:
- 订单服务的本地事务回滚(记录被删除)
- 库存服务的扣减操作已提交(因为它的本地事务已提交)
- 账户服务由于超时未执行
更糟的是,由于Feign的默认重试机制,账户服务最终收到了两次扣款请求。这个案例暴露出三个关键问题:
- 跨服务事务边界:单个服务的本地事务无法覆盖其他服务的操作
- 缺乏全局协调:各服务无法感知整个调用链的成败状态
- 重试雪崩:部分服务可能被重复调用
1.2 过渡期的临时解决方案
在全面引入分布式事务框架前,我们尝试了两种过渡方案:
方案一:本地消息表+定时任务
-- 订单表新增字段
ALTER TABLE orders ADD COLUMN tx_status VARCHAR(20);
ALTER TABLE orders ADD COLUMN retry_count INT DEFAULT 0;
// 订单服务改造
@Transactional
public void createOrder(OrderDTO order) {
// 1. 创建订单记录(初始状态为PROCESSING)
order.setStatus(OrderStatus.PROCESSING);
orderMapper.insert(order);
try {
// 2. 调用其他服务
stockFeignClient.deduct(...);
accountFeignClient.debit(...);
// 3. 成功则更新状态
order.setStatus(OrderStatus.COMPLETED);
orderMapper.updateById(order);
} catch (Exception e) {
// 记录异常信息
order.setTxStatus("FAILED");
orderMapper.updateById(order);
}
}
方案缺陷:
- 需要额外开发补偿逻辑
- 定时任务扫描频率难以平衡(太频繁影响性能,间隔长则恢复延迟)
- 无法解决服务间调用顺序的强一致性
方案二:基于RocketMQ的事务消息
// 订单服务发送半消息
TransactionSendResult sendResult = producer.sendMessageInTransaction(
new Message("order_topic", JSON.toJSONBytes(order)),
null
);
// 本地事务执行器
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
OrderDTO order = JSON.parseObject(msg.getBody(), OrderDTO.class);
orderService.createOrder(order);
return LocalTransactionState.COMMIT_MESSAGE;
} catch (Exception e) {
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}
遇到的新问题:
- 消息队列的引入增加了系统复杂度
- 某些业务场景需要保证消息消费的顺序性
- 仍然需要处理最终一致性问题
经过两周的对比测试,我们决定引入专业的分布式事务框架——Seata的AT模式。
2. Seata AT模式的引入与配置
2.1 环境准备与核心组件
我们的技术栈组合:
- 注册中心:Nacos 2.2.1
- 配置中心:Nacos(与注册中心共用)
- Seata版本:1.5.2
- 数据库:MySQL 8.0(必须使用InnoDB引擎)
关键Maven依赖:
<!-- 所有微服务都需要添加 -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.5.2</version>
</dependency>
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>2.2.1</version>
</dependency>
2.2 服务端配置详解
seata-server的nacos配置(seataServer.properties):
# 事务日志存储模式
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useSSL=false
store.db.user=seata
store.db.password=seata
store.db.minConn=5
store.db.maxConn=30
# 事务分组配置
service.vgroupMapping.default_tx_group=default
service.default.grouplist=127.0.0.1:8091
数据库表结构要求:
- 每个业务数据库需要创建
undo_log表 - Seata Server需要单独的数据库存储全局事务信息
-- 业务库需要的表
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2.3 客户端关键配置
application.yml配置要点:
seata:
enabled: true
application-id: ${spring.application.name}
tx-service-group: default_tx_group
service:
vgroup-mapping:
default_tx_group: default
grouplist:
default: 127.0.0.1:8091
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace:
group: SEATA_GROUP
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
namespace:
group: SEATA_GROUP
数据源代理配置类:
@Configuration
public class SeataDataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DruidDataSource druidDataSource() {
return new DruidDataSource();
}
@Primary
@Bean("dataSource")
public DataSource dataSourceProxy(DruidDataSource druidDataSource) {
return new DataSourceProxy(druidDataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setMapperLocations(
new PathMatchingResourcePatternResolver()
.getResources("classpath*:/mapper/*.xml"));
return factoryBean.getObject();
}
}
3. 代码改造与事务声明
3.1 全局事务注解的使用
原始代码改造对比:
// 改造前
@Transactional
public void createOrder(OrderDTO order) {
orderMapper.insert(order);
stockFeignClient.deduct(order.getProductId(), order.getQuantity());
accountFeignClient.debit(order.getUserId(), order.getAmount());
order.setStatus(OrderStatus.COMPLETED);
orderMapper.updateById(order);
}
// 改造后
@GlobalTransactional(timeoutMills = 60000, name = "createOrder")
public void createOrder(OrderDTO order) {
orderMapper.insert(order);
stockFeignClient.deduct(order.getProductId(), order.getQuantity());
accountFeignClient.debit(order.getUserId(), order.getAmount());
order.setStatus(OrderStatus.COMPLETED);
orderMapper.updateById(order);
}
关键注意事项:
@GlobalTransactional应该加在事务发起方的方法上- 超时时间需要根据业务链路的长度合理设置
- 方法内部不要捕获异常(除非明确需要特殊处理)
3.2 Feign客户端的特殊处理
为确保事务上下文传递,需要对Feign客户端进行改造:
@FeignClient(name = "account-service",
configuration = FeignConfig.class)
public interface AccountFeignClient {
@PostMapping("/account/debit")
Boolean debit(@RequestParam("userId") Long userId,
@RequestParam("amount") BigDecimal amount);
}
public class FeignConfig {
@Bean
public RequestInterceptor seataFeignInterceptor() {
return template -> {
String xid = RootContext.getXID();
if (StringUtils.isNotBlank(xid)) {
template.header(RootContext.KEY_XID, xid);
}
};
}
}
3.3 事务隔离级别的考量
Seata AT模式的隔离级别与MySQL默认的RR级别有所不同:
| 隔离问题 | AT模式解决方案 |
|---|---|
| 脏读 | 全局锁阻止未提交数据的读取 |
| 不可重复读 | 通过快照数据保证重复读取结果一致 |
| 幻读 | 不保证(业务设计应避免依赖幻读的场景) |
应对策略:
- 对于金额等敏感字段,采用
select for update显式加锁 - 在查询接口添加
@GlobalLock注解保证读取最新数据 - 业务设计上尽量避免前后依赖的连续查询
4. 上线后的监控与问题排查
4.1 Seata控制台的关键指标
我们搭建了Seata-Server的监控看板,重点关注以下指标:
-
全局事务统计:
- 每分钟事务数(TPS)
- 成功/失败比例
- 平均耗时分布
-
异常事务排查:
-- 查询失败的事务 SELECT * FROM global_table WHERE status = 3 ORDER BY begin_time DESC LIMIT 10; -- 查询对应分支事务 SELECT * FROM branch_table WHERE xid = 'xxx'; -
锁冲突检测:
-- 查询当前锁等待 SELECT * FROM lock_table WHERE row_key LIKE 'order%';
4.2 常见问题与解决方案
问题一:全局锁等待超时
错误信息:
io.seata.rm.datasource.exec.LockWaitTimeoutException: Global lock wait timeout
解决方案:
- 检查是否有长时间未提交的本地事务
- 优化业务逻辑,减少全局锁持有时间
- 调整
seata.server.max.commit.retry.timeout参数(默认值-1表示无限重试)
问题二:分支事务注册失败
错误信息:
Branch register failed: TransactionException[Failed to store branch]
排查步骤:
- 检查undo_log表结构是否正确
- 确认数据库用户有足够的权限
- 检查网络连接是否稳定
问题三:Heuristic异常
错误信息:
Heuristic termination: some branches have been committed while others have been rollbacked
处理流程:
- 通过xid查询各分支状态
- 人工核对数据一致性
- 使用Seata提供的修复工具进行补偿
4.3 性能优化实践
经过三个月运行,我们总结出以下优化经验:
-
连接池配置:
spring: datasource: druid: initial-size: 5 min-idle: 5 max-active: 20 max-wait: 60000 -
Seata Server调优:
# 增加处理线程数 server.worker.thread.max=500 # 调整日志批量写入大小 store.db.max.batch.size=100 -
客户端参数优化:
seata: client: rm: report.retry.count: 5 async.commit.buffer.limit: 10000 lock: retry.internal: 10 retry.times: 30
5. 从AT模式到其他模式的扩展思考
虽然AT模式满足了我们80%的业务场景,但在某些特殊情况下,我们也评估了其他模式:
5.1 模式对比决策矩阵
| 评估维度 | AT模式 | TCC模式 | SAGA模式 | XA模式 |
|---|---|---|---|---|
| 一致性 | 弱一致 | 最终一致 | 最终一致 | 强一致 |
| 性能 | 高(≈本地事务) | 非常高 | 高 | 低 |
| 开发成本 | 低 | 高 | 中 | 低 |
| 数据库要求 | 支持本地事务 | 无特殊要求 | 无特殊要求 | 支持XA协议 |
| 典型场景 | 订单创建 | 库存预扣 | 长流程业务 | 银行转账 |
5.2 混合模式实践案例
我们在支付结算模块采用了AT+TCC的混合模式:
@GlobalTransactional
public void settlePayment(Long orderId) {
// AT模式操作
orderService.updateStatus(orderId, PAID);
// TCC模式操作
couponService.useCouponTry(orderId);
pointService.deductPointsTry(orderId);
// 其他业务操作
accountingService.record(orderId);
}
// TCC确认阶段
@Transactional
public boolean useCouponConfirm(Long orderId) {
// 实际扣减优惠券
}
// TCC取消阶段
@Transactional
public boolean useCouponCancel(Long orderId) {
// 返还优惠券
}
这种组合方式既保持了核心链路的事务性,又在非核心环节获得了更好的性能。实际落地过程中,我们建立了完善的事务日志系统,确保任何异常都能被及时发现和处理。
更多推荐
所有评论(0)