当前位置:   article > 正文

SpringBoot之Transactional事务_springboot transactional

springboot transactional


一、事务管理方式

在Spring中,事务有两种实现方式:编程式事务管理声明式事务管理

  • 编程式事务管理编程式事务管理使用 TransactionTemplate 或者直接使用底层的 PlatformTransactionManager。对于编程式事务管理,Spring推荐使用 TransactionTemplate

  • 声明式事务管理:建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。

声明式事务管理 不需要入侵代码,通过注解 @Transactional 就可以进行事务操作,更快捷而且简单,推荐使用。


二、事务提交方式

默认情况下,数据库处于自动提交模式。每一条语句处于一个单独的事务中,在这条语句执行完毕时,如果执行成功则隐式的提交事务,如果执行失败则隐式的回滚事务。

对于正常的事务管理,是一组相关的操作处于一个事务之中,因此必须关闭数据库的自动提交模式。不过,这个我们不用担心,Spring会将底层连接的自动提交特性设置为false。也就是在使用Spring进行事物管理的时候,Spring会将是否自动提交设置为false,等价于JDBC中的 connection.setAutoCommit(false);,在执行完之后在进行提交 connection.commit();


三、事务隔离级别

隔离级别:指若干个并发的事务之间的隔离程度@Transactional 注解的 isolation 属性,可用来设置隔离级别。默认值为 Isolation.DEFAULT。Isolation枚举了多种隔离级别:

隔离级别说明
Isolation.DEFAULT这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是 TransactionDefinition.ISOLATION_READ_COMMITTED
Isolation.READ_UNCOMMITTED该隔离级别表示 一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读,不可重复读和幻读,因此很少使用该隔离级别。比如PostgreSQL实际上并没有此级别。
Isolation.READ_COMMITTED该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值。
Isolation.REPEATABLE_READ该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。该级别可以防止脏读和不可重复读。
Isolation.SERIALIZABLE所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。

四、事务传播行为

事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。@Transactional 注解的 propagation 属性可用来设置事务传播行为,默认值为 Propagation.REQUIREDPropagation 枚举了多种事务传播模式,我们以一个简单的例子来说明事务是如何传播的。

public class ServiceA {
	
    public void methodA() {
        ...
        serviceB.methodB();
        ...
    }
}
...
public class ServiceB {
    // 通过propagation属性指定methodB方法的事务传播行为
    @Transactional(propagation = ...)
    public void methodB() {
        ...
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

上面的代码中指定 methodB() 方法的不同事务传播属性值,根据 methodA() 是否开启事务,来说明 methodB() 方法的事务如何传播的。


1、Propagation.REQUIRED

业务方法需要在一个容器里运行。如果方法运行时,已经处在一个事务中,那么加入到这个事务,否则自己新建一个新的事务。

在这里插入图片描述


2、Propagation.SUPPORTS

该方法在某个事务范围内被调用,则方法成为该事务的一部分。如果方法在该事务范围外被调用,该方法就在没有事务的环境下执行。

在这里插入图片描述


3、Propagation.MANDATORY

该方法只能在一个已经存在的事务中执行,业务方法不能发起自己的事务。如果在没有事务的环境下被调用,将抛出IllegalTransactionStateException异常,此时不仅 methodB() 方法无法得到执行,也会打断 methodA() 方法的执行流程,除非在 methodA() 方法中捕获处理该异常。

在这里插入图片描述


4、Propagation.REQUIRES_NEW

不管是否存在事务,该方法总汇为自己发起一个新的事务。如果方法已经运行在一个事务中,则原有事务挂起,新的事务被创建。

在这里插入图片描述


5、Propagation.NOT_SUPPORTED

声明方法不需要事务。如果方法没有关联到一个事务,容器不会为他开启事务,如果方法在一个事务中被调用,该事务会被挂起,调用结束后,原先的事务会恢复执行。

在这里插入图片描述


6、Propagation.NEVER

该方法绝对不能在事务范围内执行。如果在,则抛出 IllegalTransactionStateException 异常,此时不仅 methodB() 方法无法得到执行,还会打断 methodA() 方法的执行流程,甚至导致 methodA() 方法发生回滚。

在这里插入图片描述


7、Propagation.NESTED

如果一个活动的事务存在,则运行在一个嵌套的事务中。如果没有活动事务,则按REQUIRED属性执行。它使用了一个单独的事务,这个事务拥有多个可以回滚的保存点。内部事务的回滚不会对外部事务造成影响。它只对DataSourceTransactionManager事务管理器起效。

例如:

  • 一旦 methodA() 方法进行回滚,则 methodB() 方法也会进行回滚。但由于该传播行为是通过数据库事务的保存点进行实现的,那么一旦 methodB() 方法抛出异常发生回滚。

  • 如果 methodA() 方法捕获了 methodB() 方法所抛出的异常,则 methodA() 就不会因此而回滚;而 methodA() 方法如果继续向上抛出异常则其也会被回滚。

在这里插入图片描述


五、事务回滚

@Transactional 注解默认只会对 unchecked异常进行回滚checked异常则不回滚

  • unchecked异常:RuntimeException(比如空指针,1/0)、Error及其子类;

  • checked异常:继承自java.lang.Exception得异常,如IOException、TimeoutException等;

如果期望对检查异常进行回滚,可通过 rollbackForrollbackForClassName 属性添加新的回滚条件:

// 方式1: 支持对所有异常类型进行回滚
@Transactional(rollbackFor = Exception.class)
// 方式2:支持对所有异常类型进行回滚
@Transactional(rollbackForClassName = {"Exception"})
  • 1
  • 2
  • 3
  • 4

类似地,还可以排除某些异常,使之不发生回滚:

// 方式1: 抛出ArithmeticException异常不进行回滚
@Transactional(noRollbackForClassName = {"ArithmeticException"} )
// 方式2: 抛出ArithmeticException异常不进行回滚
@Transactional(noRollbackFor = {ArithmeticException.class} )
  • 1
  • 2
  • 3
  • 4

六、只读事务

@Transactional 注解的 readOnly 属性默认为false,如需只读事务可将其配置为true。在只读事务中不允许执行读之外操作,否则事务将会回滚。

@Transactional(readOnly=true)
  • 1

启动事务会增加线程开销,数据库因共享读取而锁定(具体跟数据库类型和事务隔离级别有关)。通常情况下,仅是读取数据时,不必设置只读事务而增加额外的系统开销


七、解决Transactional注解不回滚

1、检查你方法是不是public的

IDEA 直接会给出提示 Methods annotated with ‘@Transactional’ must be overridable ,原理很简单,@Transactional 注解只能应用到 public 可见度的方法上。 如果应用在 protectedprivate 或者 package 可见度的方法上,也不会报错,不过事务设置不会起作用。

@Transactional
private void deleteUser() throws MyException{
    userMapper.deleteUserA();
    userMapper.deleteUserB();
}
  • 1
  • 2
  • 3
  • 4
  • 5

2、检查是不是通过类内部调用事务方法

在A类内部通过一个普通方法 methodA() 调用事务方法 methodB(),那么 methodB() 的事务会生效么?

public class A {
    public void methodA() {
        ...
        methodB();
        ...
    }

    @Transactional
    public void methodB() {
        ...
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

答案是 ,原因很简单。

这里我们将 Spring AOP 后的动态代理类 ProxyA 用伪代码的形式给出,如下所示。可以看到,虽然动态代理类 ProxyA 中的 methodB() 方法被加入了事务切面,但事实上调用 ProxyAmethodA() 方法后,会直接进入目标类A中,即执行 a.methodA() 方法,然后直接调用A类中的methodB() 方法。换言之,methodB() 方法没有通过代理类 ProxyA 进行调用,自然其事务注解不会生效。

public class ProxyA {

    private A a = new A();

    public void methodA() {
        // 执行目标方法
        a.methodA();
    }

    public void methodB() {
        // 前置增强
        ...
        // 执行目标方法
        a.methodB();
        // 后置增强
        ...
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

即使在A类的 methodA() 上也添加 @Transactional 事务注解,methodB() 方法由于没走代理类ProxyA,故 methodB() 方法依然还是使用 methodA() 方法的事务。即使将 methodB() 方法的传播行为设置为 Propagation.REQUIRES_NEW,也不会重新开启一个新的事务。因为 methodB() 方法连 @Transactional 注解都无法生效,设置传播行为更是无任何意义。

3、检查抛出的异常类型是不是unchecked异常

异常虽然抛出了,但是抛出的是非RuntimeException类型的异常,依旧不会生效。

@Transactional
public void deleteUser() throws MyException{
    userMapper.deleteUserA();
    try {
        int i = 1 / 0;
        userMapper.deleteUserB();
    } catch (Exception e) {
        throw new MyException();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

如果我想check异常也想回滚怎么办,注解上面写明异常类型即可(指定了回滚异常类型为Exception)

@Transactional(rollbackFor=Exception.class) 
  • 1

类似的还有norollbackFor,自定义不回滚的异常。


4、数据库引擎是否支持事务

如果是MySQL,注意表要使用支持事务的引擎,比如 InnoDB,如果是MyISAM,事务是不起作用的。


5、异常是不是被你catch住了

当异常被捕获后,并且没有再抛出,那么deleteUserA是不会回滚的。

@Transactional
public void deleteUser() {
    userMapper.deleteUserA();
    try {
        int i = 1 / 0;
        userMapper.deleteUserB();
    } catch (Exception e) {
        e.printStackTrace();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

6、新开启一个线程

如下的方式 deleteUserA() 也不会回滚,因为spring实现事务的原理是通过 ThreadLocal 把数据库连接绑定到当前线程中,新开启一个线程获取到的连接就不是同一个了。

@Transactional
public void deleteUser() throws MyException{
    userMapper.deleteUserA();
    try {
        //休眠1秒,保证deleteUserA先执行
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    new Thread(() -> {
        int i = 1/0;
        userMapper.deleteUserB();
    }).start();    
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/黑客灵魂/article/detail/995365
推荐阅读
相关标签
  

闽ICP备14008679号