MyBatis事务只能靠Transactional吗-MetaLite为何只保留编程式事务

MyBatis 事务只能靠 @Transactional 吗?MetaLite 为何只保留编程式事务

摘要: 多数据源项目中,@Transactional 进入方法时就可能需要确定事务管理器,但真正的数据源往往要等第一条 DAO 请求才能路由出来。MetaLite ORM 只提供编程式 TransactionManager,先进入 PENDING,再由第一次 DAO 操作绑定实际 DataSource。这牺牲了声明式事务的简洁,却换来更明确的事务边界、路由时序和排障路径。

在普通单库 Spring 项目中,@Transactional 足够好用。MetaLite 没有否定它。

问题是,当一个 DAO 面向一组主从库,甚至由租户决定具体主库时,事务开启和数据源选择之间会发生先后矛盾:事务要先开始,路由信息却可能还没有出现。

MetaLite 的选择很明确:只保留编程式事务,并把"什么时候真正绑定连接"写进事务模型。

一、事务不是注解越少越好,而是边界越清楚越好

声明式事务最容易出现的工程问题包括:

  • 自调用导致代理没有生效;
  • 捕获异常后没有继续抛出,事务没有回滚;
  • 一个大方法里夹着 RPC、消息和慢查询,事务被无意拉长;
  • 动态数据源在事务建立后才切换,实际仍使用旧连接;
  • 读写分离下,事务中的读请求错误进入从库。

编程式事务不会自动消除这些问题,但它让边界在代码中可见。

二、MetaLite 的事务状态机只有三种状态

TransactionContext 使用 ThreadLocal 保存状态:

  1. PENDING:已经进入事务回调,但尚未确定数据源;
  2. BOUND:第一次 DAO 操作已经选择并绑定 DataSource,事务正式开始;
  3. 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 进入事务时会打开 QueryToMasterSwitchJdbcTemplateManager.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

相关推荐
用户9479135811621 小时前
20260821_090153_LangGraph_生产落地的_5_个关键坑:从_State
后端
Csvn1 小时前
📊 SQL 入门 Day 21:表设计与约束
后端·sql
SamDeepThinking1 小时前
警惕那些很长时间没有编写任何代码、却在设计系统的人
java·后端·架构
aloha_1 小时前
基于Spring Boot + Vue 3的前后端一体化部署方案
后端
星火10241 小时前
【Groovy翻译-进阶篇】Groovy 中的设计模式
后端·设计模式·groovy
Zane19941 小时前
ArrayList 插入慢,LinkedList 一定快吗
java·后端
花生智源1 小时前
RAG检索优化:查询改写、重排序与缓存策略
后端
吃饱了得干活1 小时前
从类爆炸到协作——DDD战略设计登场
java·后端·架构
程序员cxuan1 小时前
我用 DeepSeek-V4-Pro,完美复刻了苹果官网
人工智能·后端·程序员