Spring 事务


简介

Spring 有五个事务隔离级别:ISOLATION_DEFAULT、ISOLATION_READ_UNCOMMITTED、ISOLATION_READ_COMMITTED、ISOLATION_REPEATABLE_READ、ISOLATION_SERIALIZABLE

第一种是 Spring 默认使用 DB设置的事务隔离级别,后面四种事务隔离级别跟 Mysql 的事务隔离级别一致,下面就类比着 Mysql 的事务隔离级别,进行分析!!!

事务并发可能产生的问题释义

脏读 ( Dirty Read )
  • 一个事务读取到另一个未提交事务修改过的数据

session B 开启一个事务,修改了 id = 1 的 name 数据,但是 session B 事务未提交;此时 另一个 session A 事务读取到 session B 修改但没提交的 name 数据,并返回了,然后 session B 事务回滚了,那么此刻 session A 读取到的数据是不存在的,那么这种现象是脏读。(读未提交–该事务隔离级别会出现)

不可重复读 (Non-Repeatable-Read)
  • 事务 A 中,多次读取同一条数据,此时事务 A 并没有结束,事务 B 对同一条数据进行了修改,事务 A 读取到 事务 B 修改后的数据,造成了 事务 A 多次读取的数据值不一致,这种现象就是不可重复读

session A 事务开启,读取到 id = 1 的 name 数据为武汉,session A 事务并未结束, session B 事务将 id = 1 的 name 数据修改为 温州,session A 事务读取 id =1 的 name 数据为温州;session B 事务将 id = 1 的 name 数据修改为杭州,sesssion A 事务读取 id = 1 的 name 数据为杭州;session B 事务每次修改后的数据,session A 事务都能读取到最新的数据,那么这种现象就是不可重复读。(读未提交,读已提交–两种事务隔离级别都可能会出现)

幻读 (Phantom)
  • 一个事务读取符合某些条件的数据, 该事务未结束,此时另一个事务插入了一些符合这些条件的新数据,这个事务再次根据这些条件读取数据时,能把另一个事务插入的数据也取出来,那么这种现象叫幻读

session A 事务开启,读取 id > 0 的 name 数据为武汉,session 事务 A 未结束;session B 事务开启,插入温州市数据;session A 事务再次读取 id > 0 的 name 数据,读取到武汉和温州时,这种现象就是幻读 (读未提交,读已提交,可重复读–三种隔离级别都可能出现)

事务隔离级别释义

DEFAULT
  • 采用 DB 默认设置的事务隔离级别
READ_UNCOMMITTED
  • 读未提交(会出现脏读,不可重复读,幻读)(隔离级别最低,并发性能最高)

session A 事务开启,更新 id = 1 的数据为温州;session B 的事务开启,读取到未提交事务 A 修改的数据;

READ_COMMITTED
  • 读已提交(会出现不可重复读,幻读)

session A 事务开启,更新 id =1 的 name 数据,session B 事务开启,读取 id = 1 的 name 数据为 武汉;session A事务提交,session B 事务再次读取 id = 1 的name 数据为温州;也就是说 事务 B 只能读取到 事务 A 修改且提交的数据

REPEATABLE_READ
  • 可重复读(会出现幻读)

session A 事务开启,更新 id = 1 的 name 名称为温州,此时 session B 事务开启,读取 id = 1 的数据为 武汉

session A 事务读取 id = 1 的 name 名称为温州;session A 事务提交;session B 事务提交;session B 事务再次读取 id = 1 的 name 名称为 温州;也就是说在可重复读隔离级别下,session B 事务读取 session A 事务修改后的值,需要 session A 事务修改数据且提交,session B 当前的事务也提交。

提问:为什么 session A事务修改数据,加了写锁,为什么别的事务还可以操作?因为 MVCC 多版本控制,有快照可以供其他事务读

SERIALIZABLE
  • 可串行化(不会出现脏读、不可重复读,幻读)(隔离级别最高,并发性能最低)
读读操作
  • 不会阻塞,两个读操作互不影响
读写操作
  • 会产生阻塞,读操作提交之后,写操作才会执行
写读操作
  • 会阻塞,写操作提交之后,读操作才会执行
写写操作
  • 会阻塞,前面的写操作提交之后,后面的写操作才会执行

事务传播行为

  • 在 Spring-tx 的 jar 包中, TransactionDefinition 接口定义了七种事务传播行为:
1. PROPAGATION_REQUIRED

​ Spring 默认设置。当前方法必须在一个具有事务的上下文中运行,如果调用端有事务在运行,那么被调用端将在该事务中运行,否则将重新开启一个事务( 如果被调用端发生异常,则调用端和被调用端事务都将回滚 )

2. PROPAGATION_SUPPORTS

​ 当前方法不必需要一个具有事务的上下文,但是上下文具有事务时,也可以在这个事务中运行

3. PROPAGATION_MANDATORY

​ 当前方法必须在一个事务中运行,不然会抛出异常

4. PROPAGATION_REQUIRES_NEW

​ 当前方法必须运行在它自己的事务中,如果一个新的事务将启动,而且如果有一个当前事务在运行的话,则当前事务( 方法 )在运行期被挂起,等待新事务提交或回滚才恢复执行

5. PROPAGATION_NOT_SUPPORTED

​ 当前方法不支持在事务中运行,总是以非事务的方式运行,如果有一个事务正在执行,当前方法将在运行期挂起,直到这个事务提交或回滚,才恢复执行

6. PROPAGATION_NEVER

​ 当前方法不支持在事务中运行,如果存在当前事务,将会抛出异常

7. PROPAGATION_NESTED

​ 如果当前方法存在一个事务正在执行,那么该方法应该运行在一个嵌套事务中,被嵌套的事务(子事务)可以独立于被封装的事务(父事务)之外提交或回滚。如果封装事务存在,并且外层事务抛出异常,事务回滚,那么内层事务必须回滚,反之,内层事务并不影响外层事务。如果封装事务不存在,则跟 PROPAGATION_REQUIRED 的传播机制一致

事务失效的场景

1. 没有走动态代理导致事务失效的场景
1.1 访问权限问题

Java 的访问权限,主要有四种:private、default、protected 、public 权限从左往右,依次变大,如果事务方法,使用错误的访问权限,事务就会出现问题,spring-tx 的 jar 包 中 AbstractFallbackTransactionAttributeSource#computeTransactionAttribute() 方法,事务方法必须是 public 关键字修饰的访问权限,否则会返回 null,事务就会失效,所以事务方法访问权限是 private ,default ,protected 关键字修饰时,Spring不会提供事务功能

1.2 使用 final 、static 关键字修饰

如果某个方法不想被子类重写,就需要用 final 关键字修饰,Spring 的事务是基于 AOP 实现的,如果使用 final 关键字,该方法将不会被重写,则无法通过动态代理,生成代理类,添加事务;static 关键字也无法使用动态代理,生成代理类,添加事务!

1.3 方法内部调用

同一个类中的方法,直接内部调用,会导致事务失效,因为 spring 的 事务是通过 aop 实现的,同一个类中的方法,直接调用,相当于通过 this 关键字直接调用方法,没有通过代理类,添加事务,故事务失效!可以通过 AopContext#currentProxy() 获取代理对象,使用事务!!!

@Servcie
public class ServiceA {

   public void save(User user) {
         queryData1();
         queryData2();
         ((ServiceA)AopContext.currentProxy()).doSave(user);
   }

   @Transactional(rollbackFor=Exception.class)
   public void doSave(User user) {
       addData1();
       updateData2();
    }
 }
1.4 没有被 Spring 管理

使用 Spring 的事务,需要Bean 对象被 Spring 管理。没有使用@Controller @Service @Reponsitory @Component @Bean 等注解,对象将不会被 Spring 管理,则事务不生效

2. 异常导致事务失效的场景
2.1 自己捕获异常

Spring 事务需要正常回滚,必须抛出 Spring 能正确处理的异常,如果没有异常抛出,则事务不会回滚

@Slf4j
@Service
public class UserService {

    @Transactional
    public void add(UserModel userModel) throws Exception {
        try {
             saveData(userModel);
             updateData(userModel);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }
}
2.2 手动抛出受检异常/非受检异常(RuntimeException)或错误(Error)

如果代码捕获了异常,并手动抛出了异常:Exception ,对于普通的 Exception (非运行时异常),事务不会回滚

@Slf4j
@Service
public class UserService {

    @Transactional
    public void add(UserModel userModel) throws Exception {
        try {
             saveData(userModel);
             updateData(userModel);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new Exception(e);
        }
    }
}
2.3 自定义回滚异常

@Transactional 注解中的 rollbackFor 属性使用默认值,那么当程序抛出 Exception ,事务不会回滚,所以,一般需要设置 rollbackFor 的属性值为 Exception 或 Throwable

@Slf4j
@Service
public class UserService {

    @Transactional(rollbackFor = BusinessException.class)
    public void add(UserModel userModel) throws Exception {
       saveData(userModel);
       updateData(userModel);
    }
}

文章作者: Huowy
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Huowy !
评论
  目录