Spring事务控制

83 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第12天,点击查看活动详情

1.JDBC

JDBC对数据库查询的简单流程如下所示:

public static void testDB(){
    System.setProperty("jdbc.drivers","com.mysql.jdbc.Driver");
    //Class.forName("com.mysql.jdbc.Driver");

    try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "eternal3210");
         Statement stmt = conn.createStatement();
         PreparedStatement pStmt = conn.prepareStatement("update user set username = '张三' where id = ?")){

        pStmt.setInt(1,1);
        int count = pStmt.executeUpdate();
        System.out.println(count);

        String sql = "select * from user";
        ResultSet resultSet = stmt.executeQuery(sql);

        while(resultSet.next()) {
            int id = resultSet.getInt(1);
            String username = resultSet.getString("username");
            int age = resultSet.getInt(3);
            System.out.println("id = " + id + "username = " + username + "age = " + age);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

同样,可以开启事务,创建保存点来回滚

conn.setAutoCommit(false);
stat.executeUpdate(command1);
Savepoint svpt = conn.setSavepoint();
stat.executeUpdate(command2);
conn.rollback(svpt);
//释放保存点
conn.releaseSavepoint(svpt);
conn.setAutoCommit(true);
conn.commit();

2.MySQL事务

MySQL InnoDB 引擎默认是可重复读级别的,不同的隔离级别可能会引发不同的问题:

  • 脏读:当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,另外一个事务也访问这个数据,且使用了这个数据。
  • 虚读 (不可重复读):在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,第一个事务两次读到的的数据可能是不一样的。
  • 幻读:第一个事务对一个表中的数据进行了修改,这种修改往往涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么操作第一个事务的用户再次查询发现表中存在没有修改的数据行,幻读是数据记录在两次读取之间发生了改变。
隔离级别/问题脏读 (Dirty Read)虚读(不可重复读) (NonRepeatable Read)幻读 (Phantom Read)
读未提交 (Read uncommitted)
读已提交 (Read committed)x
可重复读 (Repeatable read)xx
可串行化 (Serializable)xxx

3.Spring事务

3.1.编程式事务

Spring 对 JDBC的事务进行了封装,通过 TransactionManager 统一管理,示例程序如下:

Maven依赖:

org.springframework spring-jdbc 5.2.18.RELEASE 示例程序:
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring-tx.xml");
DataSourceTransactionManager transactionManager = (DataSourceTransactionManager) context.getBean("txManager");
//开启事务,接下来直到commit之前的代码都会被同一个事务所控制,调用其他方法也是一样被该事务控制
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());

try{
    //. . . . . .
    Object savepoint1 = status.createSavepoint();
    //. . . . . .
    Object savepoint2 = status.createSavepoint();
    //. . . . . .
    status.rollbackToSavepoint(savepoint2);  //回滚保存点
}catch(Exception e){
    transactionManager.rollback(status);  //回滚
}

//事务结束
transactionManager.commit(status);

编程式事务很少用到,日常开发中通常使用注解的声明式事务。

3.2.注解声明式事务

  • 事务的传播行为:

Spring提供了注解 @Transactional 声明式事务,并设置事务的传播类型,主要用于事务方法中调用其他方法的处理方式。

类别传播类型说明
支持当前事务PROPAGATION_REQUIRED支持当前事务,如果当前没有事务,就新建一个事务 (最常用) 。
PROPAGATION_SUPPORTS支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY支持当前事务,如果当前没有事务,就抛出异常。
不支持当前事务PROPAGATION_REQUIRES_NEW新建事务,如果存在当前事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED以非事务方式执行,如果当前存在事务,把当前事务挂起。
PROPAGATION_NEVER以非事务方式执行,如果当前存在事务,则抛出异常。
嵌套事务PROPAGATION_NESTED如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行 REQUIRED 类似的操作。

说明:

支持当前事务,意为加入当前事务,即开启了事务的 A 方法中调用了 B 方法,二者为方法 A 的同一个事务。 不支持当前事务,意为不加入当前事务,以 PROPAGATION_REQUIRES_NEW 为例,源码中可以看出,B 方法开启了一个新的数据库连接,即二者永远不会是同一个连接。 嵌套事务,是已经存在事务的一个真正的子事务,嵌套事务开始执行时,它将取得一个 savepoint,如果这个嵌套事务失败,我们将回滚到此 savepoint。嵌套事务是外部事务的一部分,只有外部事务结束后它才会被提交。 一般来说,我们在日常开发中,通常都会使同一个方法内为同一个事务,即方法内的数据库操作成功与失败都是一致的。

3.3.Spring事务失效

  • 现象:在同一个实现类中,使用注解 @Transactional 声明式事务声明两个方法A、B,测试类中,调用方法A,方法A中调用方法 B,B 事务配置会失效。

  • 本质原因:声明式事务本质上是通过 AOP 动态代理来实现的,而类内方法调用会绕过动态代理直接调用,在同一个类之中,方法互相调用,切面无效 ,而不仅仅是事务。这里事务之所以无效,是因为 Spring 的事务是通过 Aop 实现的。

  • 解决办法:可以让该类依赖自身,使用 bean 容器注入,调用的时候使用自身依赖调用即可。

  • 其他失效情况:

    1. Spring 的事务注解 @Transactional 只能放在 public 修饰的方法上才起作用,如果放在其他非 public (private,protected) 方法上,虽然不报错,但是事务不起作用。
    2. 如果采用 Spring + Spring MVC,则 context:component-scan 重复扫描问题可能会引起事务失败。
    3. 如使用 MySQL 且引擎是 MyISAM,则事务会不起作用,原因是 MyISAM 不支持事务,可以改成 InnoDB 引擎。
    4. @Transactional 注解开启配置,必须放到 listener 里加载,如果放到 DispatcherServlet 的配置里,事务也是不起作用的。
    5. Spring 团队建议在具体的类 (或类的方法) 上使用 @Transactional 注解,而不要使用在类所要实现的任何接口上。在接口上使用 @Transactional 注解,只能当你设置了基于接口的代理时它才生效。因为注解是不能继承的,这就意味着如果正在使用基于类的代理时,那么事务的设置将不能被基于类的代理所识别,而且对象也将不会被事务代理所包装。
    6. 在业务代码中如果抛出 RuntimeException 异常,事务回滚;但是抛出Exception,事务不回滚。
    7. 如果在加有事务的方法内,使用了 try...catch 语句块对异常进行了捕获,而 catch 语句块没有 throw new RuntimeExecption 异常,事务也不会回滚。

注释:

Spring事务的源码以目前的水平还看不懂,只能大概知道其基本原理,如:不支持当前事务的传播行为本质上是开启了不同的连接,等等。