Spring编程式事务全解析:从DataSource到TxManager再到TxTemplate

Spring编程式事务全解析:封装流程、原理与常见疑问

Spring的事务管理是个让人又爱又恨的东西,声明式事务(@Transactional)用着方便,但不够灵活;编程式事务则完全放开手脚,适合复杂场景。今天我们从最底层的实现聊起,逐步走进封装的编程式事务,剖析它与声明式的关系,再解答一个常见疑问:为什么查询不同Repository不能放在同一个@Transactional里?通过代码、表格和分析,咱们把这些问题掰开揉碎聊清楚。


从最底层开始:朴素的DataSource事务

咱们先从最原始的方式入手,用DataSource直接操作事务,假设要保存用户信息:

java 复制代码
@Service
public class UserService {
    @Autowired
    private DataSource dataSource;

    public void saveUser(String username) {
        try (Connection conn = dataSource.getConnection()) {
            conn.setAutoCommit(false);
            PreparedStatement ps = conn.prepareStatement("INSERT INTO users (username) VALUES (?)");
            ps.setString(1, username);
            ps.executeUpdate();
            conn.commit();
        } catch (SQLException e) {
            try (Connection conn = dataSource.getConnection()) {
                conn.rollback();
            } catch (SQLException ex) {
                throw new RuntimeException("Rollback failed", ex);
            }
            throw new RuntimeException("Save failed", e);
        }
    }
}

这代码很直白:拿Connection,关自动提交,手动执行SQL,成功就提交,失败就回滚。但问题也明显:

  • 代码量大:手动管理连接和异常,太繁琐。
  • 资源管理麻烦:得自己释放连接,容易漏。
  • 不灵活:没法跟Spring的其他机制无缝衔接。

第一步封装:PlatformTransactionManager

Spring提供了PlatformTransactionManager,把事务逻辑抽象出来,咱们改写一下:

java 复制代码
@Service
public class UserService {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Autowired
    private PlatformTransactionManager txManager;

    public void saveUser(String username) {
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        TransactionStatus status = txManager.getTransaction(def);

        try {
            jdbcTemplate.update("INSERT INTO users (username) VALUES (?)", username);
            txManager.commit(status);
        } catch (Exception e) {
            txManager.rollback(status);
            throw e;
        }
    }
}
  • 封装意义TxManager接管了Connection的获取、提交和回滚,底层还是JDBC,但开发者不用直接碰Connection
  • 进步:事务配置(传播行为、隔离级别)可定制,资源管理交给Spring。
  • 不足:还是得手动提交和回滚,代码不够简洁。

第二步封装:TransactionTemplate

再进一步,Spring提供了TransactionTemplate,直接把提交回滚包起来:

java 复制代码
@Service
public class UserService {
    @Autowired
    private TransactionTemplate txTemplate;
    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void saveUser(String username) {
        txTemplate.execute(status -> {
            jdbcTemplate.update("INSERT INTO users (username) VALUES (?)", username);
            return null;
        });
    }
}
  • 封装意义:事务的生命周期(开启、提交、回滚)全自动化,异常时自动回滚。
  • 进步:代码简洁到飞起,专注业务逻辑就好。
  • 联系声明式@Transactional其实就是用AOP把类似TransactionTemplate的逻辑加到方法前后,Spring核心的事务机制一脉相承。

封装流程表格

层级 操作方式 封装内容 带来的好处 局限性
DataSource 直接用Connection控制事务 无封装,纯手动操作 完全掌控,灵活性高 代码繁琐,资源管理复杂
TxManager PlatformTransactionManager管理 抽象事务定义和状态 配置灵活,与Spring集成 仍需手动提交回滚
TxTemplate TransactionTemplate执行 自动提交回滚,回调式API 简洁高效,专注业务 粒度仍受限于单一事务

每一步封装都在减少重复劳动,提升开发效率,同时保留必要的灵活性。DataSource是基础,TxManager是桥梁,TxTemplate是现代编程式事务的标配。


复杂场景:为什么查询不同Repository不行?

现在聊聊一个常见疑问:为什么查询不同的Repository不能放在同一个@Transactional里?比如:

java 复制代码
@Service
public class UserService {
    @Autowired
    private UserRepository userRepo; // 数据源1
    @Autowired
    private OrderRepository orderRepo; // 数据源2

    @Transactional
    public void process(String username) {
        User user = userRepo.findByUsername(username); // 数据源1
        Order order = orderRepo.findByOrderId(1L);    // 数据源2
        // 业务逻辑
    }
}

运行后可能会发现,第二个查询压根没被事务管理,甚至报错。这是啥情况?

是数据源不同的问题吗?

大部分时候是的。Spring的事务默认绑定到一个TransactionManager,而每个TransactionManager通常对应一个DataSource。上面例子中:

  • @Transactional默认用userRepo的数据源事务管理器。
  • orderRepo用的是另一个数据源,事务管不到它。

如果没配置多数据源支持(比如JTA),第二个查询要么跑在独立连接上,要么压根没事务。这会导致:

  • 一致性问题:异常时只有第一个数据源回滚。
  • 性能隐患:多个连接没统一管理,可能锁冲突。
同数据源不同表呢?

如果userRepoorderRepo用的是同一个DataSource,那没问题,同一个事务完全可以管多个表:

java 复制代码
@Transactional
public void process(String username) {
    User user = userRepo.findByUsername(username); // 表users
    Order order = orderRepo.findByOrderId(1L);    // 表orders
    // 正常事务管理
}

只要是同一DataSource,事务没毛病。所以,关键不是"不同表",而是"不同数据源"。


优化方向:解决多数据源问题

面对多数据源,咱们得优化:

  1. 指定TransactionManager
java 复制代码
@Transactional(transactionManager = "userTxManager")
public void processUser(String username) {
    userRepo.findByUsername(username);
}

@Transactional(transactionManager = "orderTxManager")
public void processOrder(Long orderId) {
    orderRepo.findByOrderId(orderId);
}

分开事务,各自管理。

  1. 用编程式事务
java 复制代码
public void process(String username, Long orderId) {
    TransactionTemplate userTx = new TransactionTemplate(userTxManager);
    TransactionTemplate orderTx = new TransactionTemplate(orderTxManager);

    userTx.execute(status -> {
        userRepo.findByUsername(username);
        return null;
    });
    orderTx.execute(status -> {
        orderRepo.findByOrderId(orderId);
        return null;
    });
}

细粒度控制,每个数据源独立。

  1. 分布式事务(JTA) : 如果需要统一管理多数据源,用AtomikosBitronix实现XA事务:
java 复制代码
@Transactional // JTA事务管理器
public void process(String username, Long orderId) {
    userRepo.findByUsername(username);
    orderRepo.findByOrderId(orderId);
}

所有数据源一致提交或回滚。


总结

DataSourceTxManager再到TxTemplate,Spring编程式事务的封装让代码从繁琐走向优雅,每层都在解决资源管理、配置灵活性和开发效率的问题。它跟声明式事务共享核心逻辑,只是控制权交给了开发者。至于查询不同Repository的坑,主要源自数据源差异,同源不同表完全没问题。优化方向上,细粒度可以用TxTemplate,多数据源可以用JTA,具体看场景需求。希望这篇聊下来,你对事务的"前世今生"更有感觉了!

相关推荐
why1512 小时前
腾讯(QQ浏览器)后端开发
开发语言·后端·golang
浪裡遊2 小时前
跨域问题(Cross-Origin Problem)
linux·前端·vue.js·后端·https·sprint
声声codeGrandMaster2 小时前
django之优化分页功能(利用参数共存及封装来实现)
数据库·后端·python·django
呼Lu噜2 小时前
WPF-遵循MVVM框架创建图表的显示【保姆级】
前端·后端·wpf
bing_1583 小时前
为什么选择 Spring Boot? 它是如何简化单个微服务的创建、配置和部署的?
spring boot·后端·微服务
学c真好玩3 小时前
Django创建的应用目录详细解释以及如何操作数据库自动创建表
后端·python·django
Asthenia04123 小时前
GenericObjectPool——重用你的对象
后端
Piper蛋窝3 小时前
Go 1.18 相比 Go 1.17 有哪些值得注意的改动?
后端
excel3 小时前
招幕技术人员
前端·javascript·后端
盖世英雄酱581363 小时前
什么是MCP
后端·程序员