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 本地事务。
调用入口是:
java
transactionManager.doInTransaction(() -> {
orderDao.insert(order);
accountDao.updateById(accountId, update);
return null;
});
进入回调时并不会立即从某个连接池取连接。第一次 DAO 写操作经过 JdbcTemplateManager 完成路由,然后 TransactionContext.bindAndStartTransaction 使用该 DataSource 创建 DataSourceTransactionManager。
三、为什么"第一次 DAO 绑定"很重要
假设同一套 DAO 可以按租户访问不同数据库。如果事务入口在 Service 层就固定了数据源,路由决策只能被迫提前,或者依赖隐式 ThreadLocal 切换。
延迟绑定让第一条真实的数据访问决定事务位置:
text
进入事务回调
↓
PENDING,尚未拿连接
↓
第一次 DAO 执行 DbRouter
↓
绑定实际 DataSource,开启本地事务
↓
BOUND,后续 DAO 必须使用同一 DataSource
这也建立了一条不可突破的边界:绑定后如果另一个 DAO 路由到不同 DataSource,本地事务不能假装仍然原子。
四、事务内查询为什么强制回主库
TransactionManager 进入事务时会打开 QueryToMasterSwitch。JdbcTemplateManager.getReadJdbcTemplate 检测到开关后,不再选择从库,而是转到写库路由。
原因很实际:主从复制通常存在延迟。事务里刚更新一条数据,下一条查询若进入从库,可能读到旧值。
将这个规则放进事务入口,比要求每个业务开发者记住"这次查询要手动走主库"更可靠。
五、隔离级别、传播行为和超时仍然可以配置
TransactionSettings 暴露:
isolationLevel;propagationBehavior;timeout。
它们最终写入 Spring 的 DefaultTransactionDefinition,底层事务仍由 DataSourceTransactionManager 执行。
不过要注意:这些参数不等于 MetaLite 自己实现了一套事务引擎。它只是以编程式入口控制绑定时机,再复用 Spring JDBC 的事务能力。
还有一个必须按当前源码说明的限制:TransactionContext 只保存单个 ThreadLocal 状态,新的 doInTransaction 会重新设置该状态。因此,虽然 PropagationBehaviorEnum 声明了 Spring 的多种传播取值,当前版本不能直接据此推导出"嵌套调用已经完整支持全部传播语义"。在补充嵌套上下文栈与专项测试前,应把推荐用法限制为清晰、非嵌套的单层事务回调。
六、只提供编程式事务的收益与代价
收益:
- 事务范围在代码中一眼可见;
- 延迟到第一次 DAO 才选择数据源;
- 事务中的读请求统一回主库;
- 提交、回滚、ThreadLocal 清理集中在
finally中; - 没有 DAO 操作时会记录警告,不创建无意义事务。
代价:
- 写法比一个注解更长;
- 团队必须约束回调粒度;
- 现有大量依赖
@Transactional的代码迁移成本较高; - 它仍然是单 DataSource 本地事务,不能解决跨库原子性。
因此,"只能编程式事务"是 MetaLite 的主动取舍,不应该宣传成所有项目都更优。
七、如何避免把 RPC 放进长事务
推荐把远程调用放在事务外,先准备数据,再进入短事务完成必要写入:
java
RemoteResult result = remoteClient.query(request);
transactionManager.doInTransaction(() -> {
orderDao.updateById(orderId, buildUpdate(result));
auditDao.insert(buildAudit(result));
return null;
});
如果业务必须在本地提交后发消息,应考虑事务消息、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 系列内容将持续更新,围绕核心设计、源码链路、技术取舍与生产实践展开。欢迎关注作者,及时获取后续内容。
在线演示 演示地址: admin.metalite.top/ 演示账号: guess 演示密码: admin@2026