跳到主要内容

浅谈 Transaction 事务

20211214日 创建     

1. 事务的 ACID 原则

事务的四个基本原则 (ACID) :

  • Atomicity 原子性
  • Consistency 一致性
  • Isolation 隔离性
  • Durability 持久性

原子性,指的是事务内的操作具有原子性,不可分割,不可被打断;

一致性,保证数据的一致性;

隔离性,指的是事务与事务之间应该隔离,而不能相互影响。所以,数据库的设计者设计了四种隔离级别;

持久性,一旦提交的事务操作要持久性的保存到数据库中。

事务就是让数据正确无误地持久化到数据库中。


2. 数据库并发操作产生的问题

数据库操作肯定不是串行的,同步的执行,在大多数场合都是并发地处理操作,所以,这就会引发一些数据不一致问题。例如,两个人同时操作相同的数据,还有人在此时读取这条数据,怎么保证修改后的数据是正确的,读取的数据也是正确的?

2.1 产生的问题

先来说说数据库并发操作产生的问题:

  • Dirty Read 脏读
  • Unrepeatable Read 不可重复读
  • Phantom Read 幻读
问题原因允许
脏读事务 A 读取了事务 B 未提交的数据,并在这个基础上又做了其他操作。不被允许
不可重复读事务 A 读取了事务 B 已提交的更改数据。大多数场景允许
幻读事务 A 读取了事务 B 已提交的新增数据。大多数场景允许

脏读是肯定不被允许的,这是由于事务之间没有隔离,并发操作下可能会导致数据不一致的问题。

不可重复度 和 幻读 一般是被允许的,因为你已经提交事务了,我在操作或者读取一般都是没有问题的,这符合一般情况,只有极端使用场景需要避免这种情况。

那如何解决这三种并发问题呢?数据库设计者搞了事务的隔离级别,这也是保证 ACID 原则中的隔离性,目的是为了保证数据一致性。

2.2 事务的隔离级别

四种隔离级别:

  • READ_UNCOMMITTED 读取未提交
  • READ_COMMITTED 读取已提交
  • REPEATABLE_READ 可重复读
  • SERIALIZABLE 串行化

越往下,级别越高,并发性越差,安全性越高。

每种隔离级别能解决的问题:

事物隔离级别脏读不可重复度幻读
READ_UNCOMMITTED允许允许允许
READ_COMMITTED禁止允许允许
REPEATABLE_READ禁止禁止允许
SERIALIZABLE禁止禁止禁止

一般隔离级别可以设置为 READ_COMMITTED ,解决脏读问题。


3. Spring 事务传播行为

除了事务的隔离级别外,Spring 框架还提供了 7 种事务传播行为,来控制方法调用之间的事务行为。

Spring 提供了 7 种事务传播行为:(这几个单词的直译就能体现它们的作用)

  • Propagation.REQUIRED 当前方法必须以事务方式执行
  • Propagation.SUPPORTS 当前方法支持其它方法的事务(本身没有事务)
  • Propagation.MANDATORY 当前方法强制其它方法必须以事务方式执行
  • Propagation.REQUIRES_NEW 当前方法需要以一个新的事务执行
  • Propagation.NOT_SUPPORTED 当前方法不支持事务方式执行
  • Propagation.NEVER 当前方法绝不可能以事务方式执行,也不允许其它方法以事务方式执行
  • Propagation.NESTED 当前方法作为一个子事务嵌套在其他事务中,提交还是回滚受其它事务影响

网上介绍事务传播行为的文章有很多,我就不多做介绍了。

这里介绍下 Spring 中事务的执行流程,传播行为在日常开发中使用不多,多数场景使用默认配置,所以了解即可。如果想进一步理解,可以读读代码,看下 Spring 是怎么实现的。

我画了一个草图,简述下 SpringBoot 对事务的处理流程(也是事务传播行为的实现):

(Tip: 键盘 Shift + 鼠标滑轮 可以横向滚动页面)

提示

在 SpringBoot 项目中,通常使用 @Transactional 注解为方法添加事务,通过注解的 propagation 属性(如:@Transactional(propagation = Propagation.REQUIRED) )可以设置事务的传播行为。

这个流程示例是 UserService.saveUser() 方法向 user 表中插入用户信息,并调用 UserHobbiesService.saveUserHobby() 方法向 user_hobbies 中插入用户爱好信息。

  1. 使用 CGLib 动态代理创建 UserService 代理对象并调用方法 saveUser() 的代理方法,代理的作用是方法拦截,让方法以事务方式执行。(这一步就是给 saveUser() 加了 AOP 的环绕增强,具体是否以事务执行,还要看其设置的传播行为);
  2. 代理方法执行会调用事务拦截器的 invoke() 方法,再调用 TransactionAspectSupport.invokeWithinTransaction() 方法,这一步是拿到事务的基本属性,再根据当前的事务管理器类型(我这里是 JdbcTransactionManager),去做相应的处理逻辑;
  3. 再调用 TransactionAspectSupport.createTransactionIfNecessary() 方法,这一步处理了一下事务名称(如果没有事务名称就把方法名作为事务名称),然后通过事务管理器对象调用 getTransaction() 方法;
  4. getTransaction() 方法中,会先判断当前是否已经存在事务。saveUser() 作为调用者,第一次执行到这里,所以当前还没有创建事务。然后根据 saveUser() 设置的事务传播行为判断是否要创建事务,如果是 Propagation.REQUIRED 就创建并开始一个新事务,如果是 Propagation.NOT_SUPPORTED 就不会创建事务,如果是 Propagation.MANDATORY 会抛出异常等等;
  5. getTransaction() 执行结束后返回事务状态,再往上一层 (TransactionAspectSupport) 返回事务信息,再调用 proceedWithInvocation() 方法,执行 saveUser() 中的代码,执行到 saveUserHobby() 时继续重复上面的处理逻辑,唯一不同的可能是到 4 步时,判断可能已经存在事务了,这时就会调用 handleExistingTransaction() 方法,根据 saveUserHobby() 设置的事务传播行为再做处理。
  6. 当整个调用链执行结束后,在 TransactionAspectSupport.invokeWithinTransaction() 方法中清除事务信息,没有发生异常就提交事务,发生异常回滚事务。

警告

事务的传播行为是作用于两个类的方法之间的,为什么是两个类之间?同一个类中的两个方法不行?

Spring 中的事务是通过代理来实现事务行为的(同 AOP),方法被同类中的方法调用,不会走方法拦截,而是被当前对象 (this) 调用的,所以被调用的方法不会走上面事务处理流程,导致不符合预期的结果。

例如:同类中,调用者是 Propagation.REQUIRED,被调用者是 Propagation.NEVER,那么这两个方法也是在同一个事务中执行,而不会因为被调用者是 Propagation.NEVER 导致异常。

提示

额外说下 @Transactional 的使用事项:

  • 被注解的方法必须是 public 修饰;
  • 该注解默认是 RuntimeException 异常才回滚,一般可以通过 @Transactional(rollbackFor = Exception.class) 指定回滚的异常;
  • 方法中不能使用 try-catch 捕获异常,否则不会正常回滚,必须抛出异常;

End.