Spring 事务详解 2021-12-03 13:19:00 编程 Spring, Java 暂无评论 663 次阅读 4268字 修改时间:2024-01-19 10:48:57 不管是在工作中,还是在面试时,经常会被 Spring 事务管理的各种问题绕晕,什么事务的传播机制、事务的隔离级别、事务的嵌套等等,大多数人 Spring 事务的理解都仅仅知道是基于AOP的动态代理,一个@Transaction 注解走天下。 今天抽空把 Spring 的事务理清楚,将来不管是面试,还是写代码,都会有所帮助。 啥叫事务呢?就是一组操作全部成功或者全部失败嘛,成功了commit,失败了rollback,小学生都知道了,今天我们讲点高级的 --- ## 事务的隔离级别 简单回顾一下事务的隔离级别,这个是数据库级别的哦,并不是Spring提供的 | 名称 | 描述 | | ------- | ------- | | 读未提交(Read uncommitted)| 所有事务都可以看到其他未提交事务的执行结果,也被称之为脏读(Dirty Read)。 | | Read Committed(读已提交)| 这是大多数数据库系统的默认隔离级别。一个事务只能看见已经提交事务所做的改变。同一事务有新的commit,同一select可能返回不同结果。| | Repeatable Read(可重读)|这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。| |Serializable(可串行化)|这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。| --- ## Spring 7种事务的传播机制 | 名称 | 描述 | | ------- | ------- | | propagation-required| 支持当前事务,如果有就加入当前事务中;如果当前方法没有事务,就新建一个事务;,这是 Spring 默认的传播机制| |propagation-supports| 支持当前事务,如果有就加入当前事务中;如果当前方法没有事务,就以非事务的方式执行;| |propagation-mandatory| 支持当前事务,如果有就加入当前事务中;如果当前没有事务,就抛出异常;| |propagation-requires_new| 新建事务,如果当前存在事务,就把当前事务挂起;如果当前方法没有事务,就新建事务;| |propagation-not-supported| 以非事务方式执行,如果当前方法存在事务就挂起当前事务;如果当前方法不存在事务,就以非事务方式执行;| |propagation-never|以非事务方式执行,如果当前方法存在事务就抛出异常;如果当前方法不存在事务,就以非事务方式执行;| |propagation-nested|如果当前方法有事务,则在嵌套事务内执行;如果当前方法没有事务,则与required操作类似;| --- ## Spring事务嵌套机制 看以下代码,当我们启动【methodB】时,由于有 `@Transational` 注解,Spring 会开启一个事务。 但是让我们调用【methodA】时,如果你以为系统也会为它启动一个事务,那就错了,实际上是没有的 ```java @Service public class ServiceA{ public void methodA(){ this.methodB(); } @Transaction public void methodB(){ commondao.updateUser(); } } ``` **原因:** Spring 在扫描bean的时候会扫描方法上是否包含`@Transactional`注解,如果包含,Spring会为这个bean动态地生成一个子类(AOP,即代理类,proxy),代理类是继承原来那个bean的。此时,当这个有注解的方法被调用的时候,实际上是由代理类来调用的,代理类在调用之前就会启动 transaction。然而,如果这个有注解的方法是被同一个类中的其他方法调用的,那么该方法的调用并没有通过代理类,而是直接通过原来的那个bean,就像通过this调用内部的普通方法,所以就不会启动transaction,我们看到的现象就是`@Transactional`注解无效。 以下是代理类伪代码 ```java class proxy$ServiceA{ ServiceA serviceA = new ServiceA(); void methodA(){ //由于没有@Transaction注解,所以不会开启事务,而是直接调用了方法,而methodA()中只是调用了this.methodB();,所以导致事务失效 serviceA.methodA(); } void methodB(){ startTransaction(); //由于methodB()有注解,所以会启动transaction serviceA.methodB(); } } ``` 解决方法: - 在methodA 上加入 transaction 注解 - 把注解加到类名上面(Spring不推荐这么做) - 将 methodB() 写到另外一个Service中 --- ## Spring事务回滚机制 默认情况下,Spring会对unchecked(Error或者RuntimeException)异常进行事务回滚;如果是checked(Exception)异常则不回滚。 **在同一个类中** 如果在同一个类中:调用【methodA】方法,如果【methodB】中发生了Exception,则 默认不会回滚。 如果发生了`RuntimeException`,则可以正常回滚。 如果希望发生`Exception`时回滚,可以设置`rollbackFor = Exception.class` ```java @Service public class ServiceA{ @Transaction public void methodA(){ this.methodB(); } @Transaction // @Transactional(rollbackFor = Exception.class) 如何需要Exception回滚,可以这么配置 public void methodB(){ commondao.updateUser(); throw new Exception(); // 抛出 Exception 默认不会回滚,虽报异常,但仍提交事务 throw new RuntimeException(); // 可以正常回滚 } } ``` --- **在不同的类中** 如果在不同的类中:【ServiceA】调用【ServiceB】的方法,如果【methodB】中发生了`Exception`,即使【methodA】不捕捉一样,它也默认不会回滚,原因和上面是一样的。 如果【methodB】发生了`RuntimeException`,由于@Transaction默认的传播机制(如果有事务存在 则使用原事务 如果不存在则开启新事务),内层方法发现异常了,会标记整个事务为`roll-back`,虽然外层方法捕获了异常,当方法执行结束时,会执行commit事务,但是此时发现已经标记异常,所有会报错: `org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only` ```java @Service public class ServiceA{ @Transaction public void methodA(){ try{ serviceB.methodB(); }catch(Exception e){ } } } @Service public class ServiceB{ @Transaction public void methodB(){ commondao.updateUser(); throw new Exception(); // 默认不会回滚 throw new RuntimeException(); // 标记事务为异常 } } ``` **解决方案** - 如果希望内层事务抛出异常时中断程序执行,直接在外层事务的catch代码块中抛出e. - 如果希望程序正常执行完毕,并且希望外层事务结束时全部提交,需要在内层事务中做异常捕获处理。 - 如果希望内层事务回滚,但不影响外层事务提交,需要将内层事务的传播方式指定为`propagation-nested`。注:`propagation-nested`基于数据库savepoint实现的嵌套事务,外层事务的提交和回滚能够控制嵌内层事- 务,而内层事务报错时,可以返回原始savepoint,外层事务可以继续提交。 标签: Spring, Java
评论已关闭