MyBatis事务只能靠Transactional吗-MetaLite为何只保留编程式事务 MyBatis 事务只能靠 Transactional 吗MetaLite 为何只保留编程式事务摘要多数据源项目中Transactional进入方法时就可能需要确定事务管理器但真正的数据源往往要等第一条 DAO 请求才能路由出来。MetaLite ORM 只提供编程式TransactionManager先进入PENDING再由第一次 DAO 操作绑定实际DataSource。这牺牲了声明式事务的简洁却换来更明确的事务边界、路由时序和排障路径。在普通单库 Spring 项目中Transactional足够好用。MetaLite 没有否定它。问题是当一个 DAO 面向一组主从库甚至由租户决定具体主库时事务开启和数据源选择之间会发生先后矛盾事务要先开始路由信息却可能还没有出现。MetaLite 的选择很明确只保留编程式事务并把“什么时候真正绑定连接”写进事务模型。一、事务不是注解越少越好而是边界越清楚越好声明式事务最容易出现的工程问题包括自调用导致代理没有生效捕获异常后没有继续抛出事务没有回滚一个大方法里夹着 RPC、消息和慢查询事务被无意拉长动态数据源在事务建立后才切换实际仍使用旧连接读写分离下事务中的读请求错误进入从库。编程式事务不会自动消除这些问题但它让边界在代码中可见。二、MetaLite 的事务状态机只有三种状态TransactionContext使用 ThreadLocal 保存状态PENDING已经进入事务回调但尚未确定数据源BOUND第一次 DAO 操作已经选择并绑定DataSource事务正式开始NONE当前线程没有 MetaLite 本地事务。调用入口是transactionManager.doInTransaction(()-{orderDao.insert(order);accountDao.updateById(accountId,update);returnnull;});进入回调时并不会立即从某个连接池取连接。第一次 DAO 写操作经过JdbcTemplateManager完成路由然后TransactionContext.bindAndStartTransaction使用该 DataSource 创建DataSourceTransactionManager。三、为什么“第一次 DAO 绑定”很重要假设同一套 DAO 可以按租户访问不同数据库。如果事务入口在 Service 层就固定了数据源路由决策只能被迫提前或者依赖隐式 ThreadLocal 切换。延迟绑定让第一条真实的数据访问决定事务位置进入事务回调 ↓ PENDING尚未拿连接 ↓ 第一次 DAO 执行 DbRouter ↓ 绑定实际 DataSource开启本地事务 ↓ BOUND后续 DAO 必须使用同一 DataSource这也建立了一条不可突破的边界绑定后如果另一个 DAO 路由到不同 DataSource本地事务不能假装仍然原子。四、事务内查询为什么强制回主库TransactionManager进入事务时会打开QueryToMasterSwitch。JdbcTemplateManager.getReadJdbcTemplate检测到开关后不再选择从库而是转到写库路由。原因很实际主从复制通常存在延迟。事务里刚更新一条数据下一条查询若进入从库可能读到旧值。将这个规则放进事务入口比要求每个业务开发者记住“这次查询要手动走主库”更可靠。五、隔离级别、传播行为和超时仍然可以配置TransactionSettings暴露isolationLevelpropagationBehaviortimeout。它们最终写入 Spring 的DefaultTransactionDefinition底层事务仍由DataSourceTransactionManager执行。不过要注意这些参数不等于 MetaLite 自己实现了一套事务引擎。它只是以编程式入口控制绑定时机再复用 Spring JDBC 的事务能力。还有一个必须按当前源码说明的限制TransactionContext只保存单个 ThreadLocal 状态新的doInTransaction会重新设置该状态。因此虽然PropagationBehaviorEnum声明了 Spring 的多种传播取值当前版本不能直接据此推导出“嵌套调用已经完整支持全部传播语义”。在补充嵌套上下文栈与专项测试前应把推荐用法限制为清晰、非嵌套的单层事务回调。六、只提供编程式事务的收益与代价收益事务范围在代码中一眼可见延迟到第一次 DAO 才选择数据源事务中的读请求统一回主库提交、回滚、ThreadLocal 清理集中在finally中没有 DAO 操作时会记录警告不创建无意义事务。代价写法比一个注解更长团队必须约束回调粒度现有大量依赖Transactional的代码迁移成本较高它仍然是单 DataSource 本地事务不能解决跨库原子性。因此“只能编程式事务”是 MetaLite 的主动取舍不应该宣传成所有项目都更优。七、如何避免把 RPC 放进长事务推荐把远程调用放在事务外先准备数据再进入短事务完成必要写入RemoteResultresultremoteClient.query(request);transactionManager.doInTransaction(()-{orderDao.updateById(orderId,buildUpdate(result));auditDao.insert(buildAudit(result));returnnull;});如果业务必须在本地提交后发消息应考虑事务消息、Outbox 或可靠事件而不是把网络不确定性塞进数据库事务。八、什么时候继续使用 Transactional 更合适如果项目是单数据源、事务边界简单、团队熟悉 Spring AOP并且没有延迟路由诉求Transactional仍然是成熟且低成本的选择。MetaLite 的编程式事务更适合多数据源路由必须在 DAO 时刻确定、读写分离规则需要统一、团队希望显式审查事务范围的系统。框架简介MetaLite 是面向企业生产环境的新一代 Java 微服务技术底座。系列文章重点分享代码背后的设计思路、技术取舍与工程实践。源码基线JDK 21、Spring Boot 3.2.9、Spring Cloud 2023.0.1、Spring Cloud Alibaba 2023.0.1.3具体组件版本以项目backend-bom为准。作者简介15 年 Spring 体系企业级开发经验专注于 Java 微服务架构、工程治理与生产实践。持续更新MetaLite 系列内容将持续更新围绕核心设计、源码链路、技术取舍与生产实践展开。欢迎关注作者及时获取后续内容。在线演示演示地址: https://admin.metalite.top/演示账号: guess演示密码: admin2026