Spring 与事务

105 阅读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_REQUIREDPROPAGATION_REQUIRES_NEW

事务定义都在 TransactionDefinition 中。

image.png

编程式事务用法

我们通常使用 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 来实现这个功能。

TransactionSynchronizationManager.unbindResource(this.dataSource)

总结

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