1. 项目概述:当一个Spring应用要同时操作多个数据库时,事务还能“兜得住”吗?
你有没有遇到过这种场景:订单系统需要往MySQL写主订单,同时往PostgreSQL同步库存变更日志,还要调用Oracle里的价格策略服务——三个库,三种事务边界,一旦中间出错,订单创建成功了但库存没扣、日志没记、价格没校验,整个业务就“半截子”卡在那儿。这不是理论假设,而是某电商中台团队在做订单履约模块重构时真实踩过的坑。他们最初用的是最朴素的方案:每个数据源各自开启事务,靠try-catch手动回滚。结果上线第三天就出现23笔订单状态不一致,DBA半夜被叫起来查binlog,运维同学盯着监控面板直叹气。问题根源不在SQL写得对不对,而在于Spring默认的@Transactional根本不知道“多数据源”这回事——它只认一个DataSource,只管一个TransactionManager,就像一个只配了一把钥匙的管家,面对三扇门,他要么只开一扇,要么干脆把三把钥匙全塞进一个口袋里乱拧。所以,“Spring如何管理多数据源事务”,本质上不是问“能不能做”,而是问“怎么让事务的原子性、一致性、隔离性、持久性(ACID)不因数据源数量增加而打折”。这个问题背后牵扯的是Spring事务抽象层的设计哲学、JTA规范的落地成本、本地事务的边界控制技巧,以及一线开发者在性能、可靠性、可维护性之间反复权衡的真实取舍。本文适合正在设计分布式事务方案的后端工程师、负责核心交易链路的架构师,以及那些刚在@Transaction注解上加了第二个数据源就发现事务失效、正对着日志发呆的中级开发。我们不讲JTA协议文档里的定义,也不堆砌XA两阶段提交的理论流程,而是从一次真实的故障复盘出发,拆解4种主流方案的实操细节、参数陷阱和压测表现,告诉你哪一种方案在QPS 800+、平均RT 45ms的订单场景下真正扛得住。
2. 多数据源事务的核心矛盾与方案选型逻辑
2.1 为什么单个@Transactional注解在多数据源下会“失灵”?
先说清楚一个常见误解:很多人以为只要在Service方法上加了@Transactional,再配置多个DataSource,Spring就会自动“智能识别”该用哪个事务管理器。这是错的。Spring事务管理器(PlatformTransactionManager)和数据源(DataSource)是一对一绑定的。当你声明:
@Bean public PlatformTransactionManager transactionManager1(@Qualifier("mysqlDS") DataSource ds) { return new DataSourceTransactionManager(ds); } @Bean public PlatformTransactionManager transactionManager2(@Qualifier("pgDS") DataSource ds) { return new DataSourceTransactionManager(ds); }Spring容器里确实存在两个事务管理器,但@Transactional注解默认只找名为"transactionManager"的Bean。如果你没显式指定,它就用第一个注册的(通常是transactionManager1),后续所有@Transactional操作都只在这个管理器下执行——也就是说,哪怕你代码里用了jdbcTemplate2操作PostgreSQL,事务上下文也只在MySQL连接上生效,PostgreSQL的操作完全游离在事务之外,成了“自动提交模式”。我见过最典型的误用是这样写的:
@Service public class OrderService { @Autowired private JdbcTemplate mysqlJdbcTemplate; @Autowired private JdbcTemplate pgJdbcTemplate; @Transactional // ❌ 默认只管mysqlJdbcTemplate对应的事务管理器 public void createOrder(Order order) { mysqlJdbcTemplate.update("INSERT INTO orders ..."); // ✅ 在事务内 pgJdbcTemplate.update("INSERT INTO logs ..."); // ❌ 独立提交,不受事务控制! } }提示:这个bug在单元测试里极难暴露,因为H2内存库默认开启auto-commit,所有操作看起来都“成功”了;但一上生产环境,PostgreSQL的独立提交行为立刻让数据不一致问题浮出水面。
2.2 四类主流方案的本质区别与适用边界
解决多数据源事务,业界有四条技术路径,它们不是简单的“新旧替代”,而是针对不同业务强度、基础设施成熟度、团队技术储备做出的务实选择。我把它们按“事务强一致性保障能力”从高到低排列,并标注每种方案在真实压测中的关键指标表现(基于某中台订单系统2023年Q4压测报告,硬件:4核8G容器,MySQL 8.0/PostgreSQL 14/Oracle 19c):
| 方案类型 | 核心机制 | ACID保障等级 | QPS上限(8核) | 平均RT增幅 | 运维复杂度 | 典型适用场景 |
|---|---|---|---|---|---|---|
| JTA/XA | 基于XA协议的两阶段提交,由事务协调器(如Atomikos)统一调度 | ★★★★★ 强一致 | 320~380 | +65%~+85% | ★★★★★ 高(需部署TM、配置XAResource) | 金融级核心账务,要求零数据不一致 |
| Seata AT模式 | 应用层代理SQL,自动生成undo_log,通过全局锁+分支事务协调 | ★★★★☆ 准实时一致(最终一致有延迟窗口) | 680~750 | +22%~+35% | ★★★☆☆ 中(需部署Seata Server、改造数据源) | 电商订单+库存+物流,允许秒级最终一致 |
| 编程式多事务管理 | 手动获取多个TransactionStatus,控制commit/rollback时机 | ★★★☆☆ 业务层强一致(依赖代码严谨性) | 920~1050 | +8%~+12% | ★★☆☆☆ 低(纯代码逻辑) | 中小系统、异构库组合简单(如MySQL+Redis)、对QPS敏感 |
| 事件驱动最终一致 | 主库事务提交后发MQ消息,下游消费更新其他库 | ★★☆☆☆ 最终一致(延迟不可控) | >1500 | +3%~+5% | ★☆☆☆☆ 极低(仅加MQ) | 日志同步、报表统计、非核心状态更新 |
选择哪一种,不能只看“听起来多高级”,而要看你的业务能否承受“不一致”的时间窗口。比如某支付系统要求“付款成功=账户余额变更+交易流水落库+风控记录生成”三者必须原子完成,差一秒都不行,那只能上JTA;而某内容平台的“发布文章→同步ES→更新Redis缓存”,允许ES延迟3秒,那Seata或事件驱动就更经济。
2.3 为什么我们最终放弃JTA,转向Seata AT模式?
某中台团队最初坚定选择了JTA方案,理由很充分:Atomikos开源成熟、Spring Boot有starter、文档齐全。但上线前压测暴露了三个硬伤:
- RT陡增不可接受:在模拟100并发下单时,平均响应时间从单库的42ms飙升至118ms,其中37ms耗在XA prepare阶段的网络往返上。而业务SLA要求P95 < 80ms。
- 死锁概率翻倍:XA协议要求所有分支事务在prepare阶段持有锁,直到commit/rollback指令到达。当MySQL分支已prepare、PostgreSQL分支还在执行时,若此时有另一个请求试图更新同一行,就会触发跨库死锁,Atomikos的死锁检测机制平均需要2.3秒才介入,远超业务容忍阈值。
- 运维黑洞:XA事务日志(transaction log)存储在本地磁盘,容器重启后若日志损坏,Atomikos无法自动恢复“in-doubt”事务,必须人工介入binlog比对,SRE同学反馈“每次发布都要提心吊胆”。
实操心得:JTA不是不好,而是它的设计哲学是“不惜代价保一致”,这与互联网高并发场景追求“快速失败+快速恢复”的思路天然冲突。除非你的业务属于银行核心系统级别,否则建议把JTA当作最后的保底方案,而不是首选。
团队转而评估Seata AT模式,核心看中两点:一是它把两阶段提交的协调逻辑下沉到Seata Server,应用端只需轻量Agent;二是AT模式的“一阶段直接提交”特性,让99%的请求在第一阶段就完成,只有极少数异常场景才走二阶段回滚,这对RT影响极小。后续压测证实:在相同并发下,Seata方案平均RT为58ms,P95稳定在76ms,且未出现跨库死锁。
3. Seata AT模式落地:从零开始搭建多数据源事务链路
3.1 环境准备与基础组件版本锁定
Seata对版本兼容性极其敏感,稍有不慎就会出现“全局事务ID找不到”或“分支事务注册失败”等玄学错误。我们采用以下经过验证的组合(2024年Q2线上稳定运行版本):
- Spring Boot: 2.7.18(注意:3.x需用Seata 1.7+,但1.7对Oracle支持不完善)
- Seata Server: 1.6.1(Docker镜像:seataio/seata-server:1.6.1)
- Seata Client (Spring Boot Starter): 1.6.1
- 数据库驱动:
- MySQL: mysql-connector-java 8.0.33(必须≥8.0.27,否则不支持XAConnection)
- PostgreSQL: postgresql 42.6.0(需开启prepareThreshold=0参数)
- Oracle: ojdbc8 19.22.0.0(注意:ojdbc11不兼容Seata 1.6.1)
注意:不要盲目升级到最新版!我们曾试过Seata 1.7.0 + Spring Boot 3.0.5,结果Oracle分支事务始终注册失败,排查三天才发现是ojdbc驱动版本不匹配导致的Connection.unwrap()空指针。生产环境务必以“稳定压倒一切”为原则,版本组合必须经完整回归测试。
3.2 Seata Server部署与关键配置解析
Seata Server是整个事务协调的核心,其配置直接影响全局事务的可靠性和性能。我们采用Docker Compose方式部署,关键配置如下(seata-server.yaml):
version: '3.8' services: seata-server: image: seataio/seata-server:1.6.1 ports: - "8091:8091" - "7091:7091" environment: - SEATA_CONFIG_NAME=file:/root/seata-config/registry.conf - STORE_MODE=db - SEATA_IP=seata-server - SERVER_NODE=1 volumes: - ./conf:/root/seata-config - ./logs:/root/logs其中registry.conf定义注册中心和配置中心,我们使用Nacos(v2.2.3)作为两者:
registry { type = "nacos" nacos { application = "seata-server" serverAddr = "nacos:8848" group = "SEATA_GROUP" namespace = "" } } config { type = "nacos" nacos { serverAddr = "nacos:8848" group = "SEATA_GROUP" namespace = "" } }最关键的file.conf(存储模式配置)需重点调整:
store { mode = "db" db { datasource = "druid" dbType = "mysql" driverClassName = "com.mysql.cj.jdbc.Driver" url = "jdbc:mysql://mysql:3306/seata?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai" user = "seata" password = "seata123" minConn = 5 maxConn = 30 globalTable = "global_table" # 全局事务表 branchTable = "branch_table" # 分支事务表 lockTable = "lock_table" # 全局锁表 queryLimit = 100 } }实操心得:
lock_table是Seata实现全局锁的关键,它记录了哪些全局事务正在占用哪些数据库行。如果这个表所在库的性能瓶颈,会导致所有全局事务阻塞。我们曾将lock_table放在业务MySQL实例上,大促期间该表写入延迟飙升至200ms,直接拖垮整个Seata集群。解决方案是:为Seata单独部署一个轻量级MySQL实例(2核4G),专用于存储global_table、branch_table、lock_table三张元数据表,彻底隔离业务压力。
3.3 应用端集成:多数据源+Seata代理的完整配置
这是最容易出错的环节。核心原则是:Seata的DataSourceProxy必须包裹原始DataSource,且每个数据源对应一个独立的DataSourceProxy。配置步骤如下:
Step 1:定义原始数据源Bean
@Configuration public class DataSourceConfig { @Bean @ConfigurationProperties("spring.datasource.mysql") public DataSource mysqlDataSource() { return DataSourceBuilder.create().build(); } @Bean @ConfigurationProperties("spring.datasource.pg") public DataSource pgDataSource() { return DataSourceBuilder.create().build(); } @Bean @ConfigurationProperties("spring.datasource.oracle") public DataSource oracleDataSource() { return DataSourceBuilder.create().build(); } }Step 2:为每个数据源创建Seata代理
@Configuration public class SeataDataSourceConfig { @Bean("mysqlDataSourceProxy") public DataSourceProxy mysqlDataSourceProxy(@Qualifier("mysqlDataSource") DataSource dataSource) { return new DataSourceProxy(dataSource); } @Bean("pgDataSourceProxy") public DataSourceProxy pgDataSourceProxy(@Qualifier("pgDataSource") DataSource dataSource) { return new DataSourceProxy(dataSource); } @Bean("oracleDataSourceProxy") public DataSourceProxy oracleDataSourceProxy(@Qualifier("oracleDataSource") DataSource dataSource) { return new DataSourceProxy(dataSource); } }Step 3:配置JdbcTemplate,指向代理数据源
@Configuration public class JdbcTemplateConfig { @Bean("mysqlJdbcTemplate") public JdbcTemplate mysqlJdbcTemplate(@Qualifier("mysqlDataSourceProxy") DataSource dataSource) { return new JdbcTemplate(dataSource); } @Bean("pgJdbcTemplate") public JdbcTemplate pgJdbcTemplate(@Qualifier("pgDataSourceProxy") DataSource dataSource) { return new JdbcTemplate(dataSource); } @Bean("oracleJdbcTemplate") public JdbcTemplate oracleJdbcTemplate(@Qualifier("oracleDataSourceProxy") DataSource dataSource) { return new JdbcTemplate(dataSource); } }Step 4:启用Seata全局事务扫描
在启动类上添加@EnableAutoDataSourceProxy(注意:不是@EnableSeata,那是老版本API):
@SpringBootApplication @EnableAutoDataSourceProxy // 启用Seata数据源自动代理 public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } }提示:
@EnableAutoDataSourceProxy会自动查找所有DataSource类型的Bean,并用DataSourceProxy包装它们。如果你有多个数据源但只想代理其中几个,必须显式定义DataSourceProxyBean(如Step 2所示),并确保@EnableAutoDataSourceProxy不启用,否则会重复代理导致ClassCastException。
3.4 业务代码编写:@GlobalTransactional的正确姿势
@GlobalTransactional是Seata的入口注解,但它和@Transactional的使用逻辑有本质区别:
@Transactional作用于单个数据源的本地事务,可以嵌套、可以传播;@GlobalTransactional作用于整个分布式事务链路,必须加在发起方(即第一个分支事务的入口方法)上,且不能嵌套在另一个@GlobalTransactional内。
典型订单创建代码如下:
@Service @Slf4j public class OrderService { @Autowired private JdbcTemplate mysqlJdbcTemplate; @Autowired private JdbcTemplate pgJdbcTemplate; @Autowired private JdbcTemplate oracleJdbcTemplate; /** * ✅ 正确:全局事务入口,所有分支事务由此发起 */ @GlobalTransactional(timeoutMills = 300000, name = "create-order-service") public void createOrder(Order order) { try { // 分支1:MySQL写订单主表 mysqlJdbcTemplate.update( "INSERT INTO orders (id, user_id, amount) VALUES (?, ?, ?)", order.getId(), order.getUserId(), order.getAmount() ); // 分支2:PostgreSQL写日志 pgJdbcTemplate.update( "INSERT INTO order_logs (order_id, status, created_time) VALUES (?, ?, ?)", order.getId(), "CREATED", System.currentTimeMillis() ); // 分支3:Oracle调用价格策略(通过JDBC直连) oracleJdbcTemplate.update( "UPDATE price_strategy SET last_used = ? WHERE product_id = ?", System.currentTimeMillis(), order.getProductId() ); } catch (Exception e) { log.error("全局事务执行失败,order id: {}", order.getId(), e); throw e; // Seata会自动触发所有分支的回滚 } } /** * ❌ 错误:在全局事务方法内再起一个@GlobalTransactional,会报错 */ @GlobalTransactional // 删除此行! public void updateInventory(String orderId) { // ... } }实操心得:
timeoutMills参数至关重要。它定义了全局事务的最大存活时间,超时后Seata Server会强制回滚。我们最初设为60秒,结果在数据库慢查询(如Oracle执行计划走错)时,事务被误杀,导致部分分支已提交、部分被回滚的“半成品”状态。后来根据压测中99.9%的事务完成时间(12.8秒),将timeout设为120秒,并配合监控告警(当全局事务持续时间>90秒时短信通知),既避免误杀,又及时发现慢SQL。
4. 编程式多事务管理:轻量级方案的深度实践
4.1 为什么在Seata之外还要掌握编程式方案?
Seata虽好,但并非银弹。它引入了额外的组件(Seata Server)、增加了网络调用、对数据库驱动有特定要求。当你的场景满足以下任一条件时,编程式方案反而更优:
- 异构数据库组合简单:比如只涉及MySQL + Redis(Redis本身无事务,但可用Lua保证原子性),或MySQL + Elasticsearch(ES不支持事务,只能用异步补偿);
- 对延迟极度敏感:QPS > 1000,且P99 RT必须< 30ms,无法承受Seata的额外网络开销;
- 基础设施受限:无法部署额外的Seata Server,或现有K8s集群资源紧张。
编程式方案的核心思想是:放弃“一个注解管所有”,改为在代码中显式控制每个数据源的事务生命周期。它不依赖任何框架,纯粹靠Spring的TransactionTemplate和TransactionStatusAPI实现。
4.2 事务模板的封装与复用
为避免每个Service方法都写冗长的事务控制代码,我们封装了一个MultiTransactionTemplate工具类:
@Component public class MultiTransactionTemplate { @Autowired private TransactionManager mysqlTxManager; @Autowired private TransactionManager pgTxManager; @Autowired private TransactionManager oracleTxManager; /** * 执行多数据源事务:所有分支必须全部成功,否则全部回滚 * @param mysqlAction MySQL分支操作 * @param pgAction PostgreSQL分支操作 * @param oracleAction Oracle分支操作 */ public void executeInMultiTransaction( TransactionCallbackWithoutResult mysqlAction, TransactionCallbackWithoutResult pgAction, TransactionCallbackWithoutResult oracleAction) { TransactionStatus mysqlStatus = null; TransactionStatus pgStatus = null; TransactionStatus oracleStatus = null; try { // 1. 分别开启各数据源事务 mysqlStatus = mysqlTxManager.getTransaction(new DefaultTransactionDefinition()); pgStatus = pgTxManager.getTransaction(new DefaultTransactionDefinition()); oracleStatus = oracleTxManager.getTransaction(new DefaultTransactionDefinition()); // 2. 执行各分支业务逻辑 new TransactionTemplate(mysqlTxManager).execute(mysqlAction); new TransactionTemplate(pgTxManager).execute(pgAction); new TransactionTemplate(oracleTxManager).execute(oracleAction); // 3. 全部成功,逐一提交 mysqlTxManager.commit(mysqlStatus); pgTxManager.commit(pgStatus); oracleTxManager.commit(oracleStatus); } catch (Exception e) { // 4. 任一分支失败,逐一回滚(注意:回滚顺序与提交顺序相反) if (mysqlStatus != null && !mysqlStatus.isCompleted()) { mysqlTxManager.rollback(mysqlStatus); } if (pgStatus != null && !pgStatus.isCompleted()) { pgTxManager.rollback(pgStatus); } if (oracleStatus != null && !oracleStatus.isCompleted()) { oracleTxManager.rollback(oracleStatus); } throw e; } } }4.3 在订单服务中的实际应用
使用封装好的模板,订单创建逻辑变得清晰且可控:
@Service @Slf4j public class OrderService { @Autowired private MultiTransactionTemplate multiTxTemplate; @Autowired private JdbcTemplate mysqlJdbcTemplate; @Autowired private JdbcTemplate pgJdbcTemplate; @Autowired private JdbcTemplate oracleJdbcTemplate; public void createOrder(Order order) { multiTxTemplate.executeInMultiTransaction( status -> { log.info("执行MySQL分支:插入订单"); mysqlJdbcTemplate.update( "INSERT INTO orders (id, user_id, amount) VALUES (?, ?, ?)", order.getId(), order.getUserId(), order.getAmount() ); }, status -> { log.info("执行PostgreSQL分支:插入日志"); pgJdbcTemplate.update( "INSERT INTO order_logs (order_id, status, created_time) VALUES (?, ?, ?)", order.getId(), "CREATED", System.currentTimeMillis() ); }, status -> { log.info("执行Oracle分支:更新价格策略"); oracleJdbcTemplate.update( "UPDATE price_strategy SET last_used = ? WHERE product_id = ?", System.currentTimeMillis(), order.getProductId() ); } ); } }注意事项:编程式方案最大的风险是回滚遗漏。比如在
oracleJdbcTemplate.update()抛出异常后,程序跳转到catch块,但如果mysqlTxManager.rollback()也因网络问题失败,MySQL事务就处于“悬挂”状态。我们的应对措施是:
- 所有
rollback()调用都加上重试(最多3次,间隔100ms);- 每日凌晨执行一个Job,扫描
information_schema.INNODB_TRX表,找出运行时间>30分钟的事务,自动告警并提供kill指令;- 在事务开启时,将
TransactionStatus对象序列化存入Redis(key为tx:${UUID}),回滚失败时可从Redis读取原始状态重试。
4.4 性能对比:编程式 vs Seata AT模式
我们在相同硬件和数据量下(10万订单压测)对比了两种方案:
| 指标 | 编程式多事务 | Seata AT模式 | 差异分析 |
|---|---|---|---|
| 平均RT | 48.2ms | 57.6ms | 编程式少2次网络调用(Seata Client → Seata Server) |
| P99 RT | 72.4ms | 85.1ms | Seata在高并发下分支注册排队导致延迟毛刺 |
| QPS | 1020 | 735 | 编程式无协调中心瓶颈,吞吐更高 |
| 事务成功率 | 99.992% | 99.998% | Seata有完善的重试和补偿机制,编程式依赖代码健壮性 |
| 故障恢复时间 | < 10秒(自动重试) | < 30秒(Seata Server自动恢复) | Seata内置恢复能力更强 |
结论很明确:如果你的团队有足够经验写出健壮的事务控制代码,且对性能有极致要求,编程式是值得投入的;如果更看重“开箱即用”和长期稳定性,Seata仍是首选。
5. 常见问题与实战排障指南
5.1 全局事务ID为空?检查这5个关键点
这是Seata接入初期最高频的报错,日志里常出现Could not found global transaction xid=xxx。排查清单如下:
- 确认
@GlobalTransactional是否加在最外层方法:它必须是整个调用链的起点,不能加在被其他@Transactional方法调用的内部方法上。 - 检查
application.yml中spring.application.name是否设置:Seata用此名称作为服务名注册到Nacos,若为空,Seata Client无法找到Server。 - 验证
file.conf中service.vgroupMapping配置:必须与代码中@GlobalTransactional的name参数匹配。例如:service { vgroupMapping.my_test_tx_group = "default" // 必须与@GlobalTransactional(name="my_test_tx_group")一致 default.grouplist = "127.0.0.1:8091" } - 检查数据库驱动版本:MySQL必须≥8.0.27,PostgreSQL必须≥42.3.0,Oracle必须≥19.3.0.0。旧版本驱动不支持
unwrap()接口,导致Seata无法获取原生Connection。 - 确认
DataSourceProxy是否正确注入:在@GlobalTransactional方法内,所有JDBC操作必须使用DataSourceProxy包装后的JdbcTemplate,如果混用了原始DataSource,Seata无法拦截SQL。
排障技巧:在
@GlobalTransactional方法入口处加一行日志:log.info("Global XID: {}", RootContext.getXID());。如果输出为null,说明全局事务上下文根本没建立,问题一定出在上述1-3点;如果输出正常但分支注册失败,则聚焦4-5点。
5.2 分支事务注册失败?90%是数据库权限或表结构问题
分支注册失败的日志特征是:BranchRegisterRequest failed, xid=xxx, branchType=AT。常见原因:
- MySQL用户缺少
SELECT权限:Seata在注册分支前会执行SELECT * FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?来获取表结构,若用户无information_schema库权限,会静默失败。 - 未创建undo_log表:每个业务库必须有
undo_log表,建表语句必须严格匹配Seata官方文档(注意xid字段长度为128,branch_id为bigint)。我们曾因复制粘贴时漏掉UNIQUE KEY ux_undo_log索引,导致高并发下分支注册大量失败。 - PostgreSQL的
prepareThreshold参数未关闭:PostgreSQL JDBC驱动默认开启预编译缓存(prepareThreshold=5),这会干扰Seata的SQL解析。必须在连接URL中显式设置prepareThreshold=0。
5.3 如何安全地进行数据库迁移?避免事务中断
当需要将MySQL分库分表,或把PostgreSQL升级到15版本时,事务链路极易中断。我们的平滑迁移三步法:
- 双写过渡期:新旧库同时写入,但只从新库读。在
MultiTransactionTemplate中,将旧库分支设为“尽力而为”(不参与回滚),新库分支为“必须成功”。 - 流量灰度:用Apollo配置中心控制
@GlobalTransactional的开关比例,从1%流量开始,逐步提升至100%,全程监控seata_branch_register_fail_rate指标。 - 回滚预案:提前准备好SQL脚本,当新库异常时,能一键将新库数据反向同步回旧库,确保业务无感。
最后分享一个小技巧:在所有
@GlobalTransactional方法上,统一加上@SentinelResource注解,配置QPS限流规则。当Seata Server出现延迟时,Sentinel会自动熔断,避免雪崩。这比单纯依赖Seata的timeout机制更主动、更可控。