Spring 是如何管理事务的

121 阅读4分钟

Spring 在 Java Web 开发中起到很重要的作用,其中 Spring 对于事务的控制又是我们日常开发中经常接触到的。这篇文章主要解释 Java 是如何控制事务以及 Spring 作为框架是如何封装事务行为的。

Java 如何控制事务

为了让 Java 与数据库更方便地交互,Sun 基于关系型数据库,抽象出一系列方便数据库操作的接口,包括 DriverManage、Connection、Session、Statement 等。

对于上层程序来说,只需要引入对应数据库的驱动即可操作数据库。对于数据库来说,只要按照 JDBC 要求的规范实现即可被操作。

我们以 MySQL 为例,在引入 MySQL 的驱动之后,就可以通过 JDBC 接口访问数据库了。

public static void main(String[] args) {
    String jdbcUrl = "jdbc:mysql://localhost:3306/test";
    String username = "root";
    String password = "root";
    try (Connection connection = DriverManager.getConnection(jdbcUrl, username, password); PreparedStatement preparedStatement = connection.prepareStatement("update user set username = 'test123' where id = 8")) {
        connection.setAutoCommit(false);
        int affectedRows = preparedStatement.executeUpdate();
        System.out.println("affectedRows:" + affectedRows);
        connection.commit();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

Spring 如何封装事务

Spring 是一个框架,框架就是通过制定规范,让使用者自动获得某些能力。下面我们看下 Spring 为开发者提供了哪些功能。

  • 声明式事务(通过 @Transaction 注解实现)
  • 事务管理器(TransactionManager
  • 事务属性配置(事务传播行为、隔离级别、超时属性、回滚等)

Spring 的封装,都是围绕着 Connection 和 Transaction 来进行的。

Connection 的管理

Connection 的产生

Spring 作为一个框架,不能依赖具体的实现。因此,获取 Connection 都是通过 JDBC 规范中的 DataSource#getConnection 得来的。DataSource 是获取连接的工厂,构造 DataSource 就属于框架使用者的责任,可以使用 HikariCP、C3P0、Druid 等连接池。

Connection 的存放

Connection 的产生与存放都是由连接池负责的,但是在从连接池中获取到事务提交这段时间内,是放在 ThreadLocal 中的。

为什么要放到 ThreadLocal 中呢?因为连接和事务是一对一绑定的。在一个事务上下文中,只允许一个 Connection 出现。

在 Spring 中,是通过 TransactionSynchronizationManager类实现的。对于当前正在执行的事务来说,都会通过 TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder()) 将当前的 dataSource 与 Connection 存放到 ThreadLocal 中。考虑到多数据源的需求,是通过 map 关联起 dataSource 和 Connection 的。

事务的管理

事务的定义

Spring 作为一个框架,目标之一便是对开发者友好。因此,Spring 仿效 EJB 也制定了事务传播行为,我们主要探讨:PROPAGATION_REQUIRED 和 PROPAGATION_REQUIRES_NEW 。

事务定义都在 TransactionDefinition 中。

图片

事务定义

编程式事务用法

我们通常使用 TransactionTemplate 来对事务进行编程操作。

transactionTemplate.execute(new TransactionCallback<Void>() {
    @Override
    public Void doInTransaction(TransactionStatus status) {
        DefaultTransactionStatus transactionStatus = (DefaultTransactionStatus) status;
        try {
            // 执行事务内的业务逻辑 
        } catch (Exception ex) {
            // 发生异常,标记事务为回滚
            status.setRollbackOnly();
        }
        return null;
    }
});

既然是 Template,那么 Spring 必然隐藏了一些与业务无关的细节。事务执行的模板就是:获取事务、执行业务逻辑、回滚(RuntimeException)或提交事务。而我们只需要关心具体的业务流程即可。

TransactionStatus status = this.transactionManager.getTransaction(this);
T result;
try {
    result = action.doInTransaction(status);
} catch (RuntimeException | Error ex) {
    // Transactional code threw application exception -> rollback   
    rollbackOnException(status, ex);
    throw ex;
} catch (Throwable ex) {
    // Transactional code threw unexpected exception -> rollback    
    rollbackOnException(status, ex);
    throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
}
this.transactionManager.commit(status);
return result;

事务的行为

Spring 根据当前事务和传播行为来获取事务。传播行为是个虚拟的概念,底层还是依靠着事务的开启和暂停实现。

开启事务

在 JDBC 编程中,我们并不需要像在命令行中一样,输入 begin 才能开启事务。只要执行了 SQL,就会自动开启事务。

唯一做的也就是将自动提交事务改变成手动提交,以方便 Spring 管理事务。

Spring 主要负责将事务属性填充到描述对象中,并将当前连接放到 ThreadLocal 中。

TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder())

获取事务

前面我们提过,事务与 Connection 在运行时是一一对应关系,获取事务,只需要从 ThreadLocal 中获取当前 dataSource 中的连接,如果存在并且是 active 状态的,就可以认为当前执行中有已经有事务存在。

TransactionSynchronizationManager.getResource(obtainDataSource())

暂停事务

因为有传播行为的存在,可能需要暂停某些事务的调用。比如当新的调用行为是 REQUIRED_NEW 时,就需要暂停当前事务,重新开一个新的事务执行。MySQL 没有提供暂停事务的功能。Spring 简单地通过让线程获取不到 Connection 来实现这个功能,就是从 ThreadLocal 中 remove 掉这个 Connection。

TransactionSynchronizationManager.unbindResource(this.dataSource)

恢复事务

恢复事务也很简单,只需要将暂停的事务信息重新放回到 ThreadLocal 中,供线程获取到当时的 Connection 即可。

需要注意的是,当事务暂停后新开事务执行完毕后,会恢复之前暂停的事务,恢复的代码会放到 finally 中实现。

TransactionSynchronizationManager.bindResource(obtainDataSource(), suspendedResources)

总结

  • Spring 作为一个框架,封装了 JDBC 的功能, 使得开发者可以更专注于自己的业务,避免关注更底层的技术实现。
  • Spring 在对事务的封装中,大量使用了模板模式。定义模板步骤,由子类实现关键细节。