事务其实是属于数据库的内容,但作为 Java 工程师,大部分场景是在 Spring 框架的加持下来使用事务的,所以关于事务的内容我就在 Spring 相关章节来总结了。

一、什么是事务?

事务是将多个原子性操作打包成一个整体并使其具有原子性,这一系列操作要么完全地执行,要么完全地不执行。

事务(Transaction),一般是指要做的或所做的事情。在计算机术语中是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。事务通常由高级数据库操纵语言或编程语言(如 SQL,C++ 或 Java)书写的用户程序的执行所引起,并用形如begin transaction和end transaction语句(或函数调用)来界定。事务由事务开始(begin transaction)和事务结束(end transaction)之间执行的全体操作组成。

二、为什么要使用事务?

事务的定义比较抽象,我们结合事务的定义和一会现实生活中的例子我们简化理解。

2.1、举例来说明

例子。

格雷福斯给贾克斯转账 300 元,业务上可以将转账操作分为两个步骤完成,详情如下:

  • 步骤一: 从格雷福斯的账户上减少300元

    1. --步骤一: 格雷福斯钱包减少300
    2. update wallet set money = money -300 where name= ‘格雷福斯’
  • 步骤二:给贾克斯的账户上增加300元

    1. --步骤二:贾克斯钱包增加300
    2. update wallet set money = money +300 where name= ‘贾克斯’

    如果在完成了步骤一的操作后突然宕机了,格雷福斯的钱减少了而贾克斯的钱没有增加那格雷福斯岂不是白白丢了300块,这时候就需要用到我们的事务了,引入事务可以保证这两个关键操作要么都成功,要么都要失败。

通过以上例子的解释相信你对为什么要使用事务的问题已经有了答案。

三、事务的特性

ACID 就是说事务能够通过 AID 来保证这个 C 的过程,C 是目的,AID 都是手段。

185b9c49-4c13-4241-a848-fbff85c03a64.png

3.1、原子性(Atomicity)

一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

3.2、一致性(Consistency)

一致性指的是系统从一个正确的状态到另一个正确的状态,这里提到的【正确状态】由使用者定义,不同的系统【正确状态】有不同的定义。以转账业务来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,那么对于此例子来说钱的总数是5000这个状态就是【正确状态】,这就是事务的一致性。

3.3、隔离性(Isolation)

数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。

3.4、持久性(Durability)

事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

四、事务的隔离级别

在多个事务同时执行时,不同的隔离级会有不同的并发事务问题。

4.1、READ-UNCOMMITTED

最低的隔离级别,允许读取尚未提交的数据变更。该级别可能会导致脏读、幻读或不可重复读。

4.2、READ-COMMITTED

允许读取并发事务已经提交的数据。该级别可以阻止脏读,但是幻读或不可重复读仍有可能发生。

4.3、REPEATABLE-READ

对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改。该级别可以阻止脏读和不可重复读,但幻读仍有可能发生。

4.4、SERIALIZABLE

完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰。该级别可以防止脏读、不可重复读以及幻读。

五、并发事务的问题

上文我们了解到了事务的隔离级别,不同隔离级别的并发事务会有不同的问题,本节对并发事务中遇到的问题进行解析。

事务隔离级别 脏读 幻读 不可重复读
READ-UNCOMMITTED
READ-COMMITTED
REPEATABLE-READ
SERIALIZABLE

5.1、脏读

脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。

当一个事务多次修改某个数据,而在这个事务中多次的修改都还未提交,这时一个并发的事务来访问该数据,就会造成两个事务得到的数据不一致。例如:用户A向用户B转账100元,对应SQL命令如下:

  1. update account set money=money+100 where name=’B’; (此时A通知B)
  2. update account set money=money - 100 where name=’A’;

当只执行第一条 SQL 时,A 通知 B 查看账户,B 发现确实钱已到账(此时即发生了脏读),而之后无论第二条 SQL 是否执行,只要该事务不提交,则所有操作都将回滚,那么当 B 以后再次查看账户时就会发现钱其实并没有转。

5.2、幻读

幻读是事务并发执行时发生的一种现象。 可以通过这句话来方便你记忆幻读:我刚刚明明读取到了三条数据,现在怎么变为两条了,是我产生了幻觉了吗?

例如事务 T1 对一个表中所有的行的某个数据项做了从1修改为2的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为1并且提交给数据库。而操作事务 T1 的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务 T2 中添加的,就好像产生幻觉一样,这就是发生了幻读。

【幻读】和【不可重复读】都是读取了另一个事务提交的数据,所不同的是【不可重复读】查询的都是同一个数据项,而幻读针对的是一批数据整体。

5.3、不可重复读

不可重复读是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。

例如事务T1在读取某一数据,而事务T2立马修改了这个数据并且提交事务给数据库,事务T1再次读取该数据就得到了不同的结果,发生了不可重复读。  

不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。
  
在某些情况下,不可重复读并不是问题,比如我们多次查询某个数据当然以最后查询得到的结果为主。但在另一些情况下就有可能发生问题,比如你在做一次查询,查询执行时间点之后插入数据库的数据不允许查询到,这种情况下不可重复读就会对业务造成影响。

5.4、丢失修改

指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 例如:事务1读取某表中的数据 A=20,事务2也读取 A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。

六、Spring 事务的传播行为

事务传播行为(propagation behavior)指的就是当一个事务方法 A 被另一个事务方法 B 调用时,事务方法 B 应该如何进行。

methodA 事务方法调用 methodB 事务方法时,methodB 是继续在调用者 methodA 的事务中运行,还是为自己开启一个新事务运行,这就是由 methodB 的事务传播行为决定的。在 Spring 中事务的传播行为包含如下几种方式。

事务传播行为类型 说明
PROPAGATION_REQUIRED 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。
PROPAGATION_SUPPORTS 支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY 使用当前的事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRES_NEW 新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER 以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。

6.1、PROPAGATION_REQUIRED

如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。

6.1.1、举例说明
  1. @Transactional(propagation = Propagation.REQUIRED)
  2. public void methodA() {
  3. methodB();
  4. //do something
  5. }
  6. @Transactional(propagation = Propagation.REQUIRED)
  7. public void methodB() {
  8. //do something
  9. }

单独调用 methodB 方法时,因为当前上下文不存在事务,所以会开启一个新的事务。 调用methodA方法时,因为当前上下文不存在事务,所以会开启一个新的事务。当执行到methodB时,methodB发现当前上下文有事务,因此就加入到当前事务中来。

6.2、PROPAGATION_SUPPORTS

如果当前存在事务,则加入该事务,如果当前没有事务,则以非事务的方式继续运行。

  1. @Transactional(propagation = Propagation.REQUIRED)
  2. public void methodA() {
  3. methodB();
  4. // do something
  5. }
  6. // 事务属性为SUPPORTS
  7. @Transactional(propagation = Propagation.SUPPORTS)
  8. public void methodB() {
  9. // do something
  10. }

单纯的调用 methodB 时,methodB 方法是非事务的执行的。当调用methdA时,methodB则加入了methodA的事务中,事务地执行。

6.3、PROPAGATION_MANDATORY

如果当前存在事务,则加入该事务,如果当前没有事务,则抛出异常。(mandatory:强制性)

  1. @Transactional(propagation = Propagation.REQUIRED)
  2. public void methodA() {
  3. methodB();
  4. // do something
  5. }
  6. // 事务属性为MANDATORY
  7. @Transactional(propagation = Propagation.MANDATORY)
  8. public void methodB() {
  9. // do something
  10. }

当单独调用 methodB 时,因为当前没有一个活动的事务,则会抛出异常IllegalTransactionStateException。当调用methodA 时,methodB则加入到methodA的事务中,事务地执行。

6.4、PROPAGATION_REQUIRES_NEW

创建一个新的事务,如果当前存在事务,则把当前事务挂起。

6.5、PROPAGATION_NOT_SUPPORTED

以非事务方式运行,如果当前存在事务,则把当前事务挂起。

6.6、PROPAGATION_NEVER

以非事务方式运行,如果当前存在事务,则抛出异常。

6.7、PROPAGATION_NESTED

如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED

参考

1、Spring 事务总结 @JavaGuide
2、事务属性之7种传播行为 @CSDN唐大麦