一、事务详解
1、事务定义
1、在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务。
2、事务是用来维护数据库的完整性,保证成批的 SQL 语句要么全部执行,要么全部不执行。
3、事务需要满足4个条件(ACID):原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)
4、mysql默认自动提交事务
2、事务四大特性
1、原子性
一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。 |
2、一致性
在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。 |
3、隔离性
数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable) |
4、持久性
事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。 |
3、事务隔离级别
1、什么是事务隔离性
多个人在同一个类里写代码,最后肯定会造成代码冲突,多个事务同时操作一个数据也会和上面的情况类似,所以为了让不同的事务之间相互不存在干扰,就需要对事务的操作进行隔离,事务的隔离性也就是将操作同一个数据的事务相互分离,让操作之间分开有序的执行。 |
2、用什么方式实现事务的隔离性
通常数据库里都是采用锁的机制,保证事务之间的隔离性,在一个事务对数据进行修改的时候,首先会对该数据进行加锁,在当前事务没有释放锁之前,后续的事务是无法对该数据再次进行加锁的,所以其它事务只能等待,只有前一个事务释放了锁之后,后面的事务才能进行加锁。通过加锁的方式来保证这种先来后到的顺序,以隔离多个事务对数据的操作,从而实现事务的隔离性。 |
4、隔离级别详解
Mysql默认隔离级别是可重复读。在事务并发执行的时候,如果不进行事务隔离,那么就会产生脏写、脏读、重复读、幻读的问题。 为了解决这些问题,数据库也针对不同的场景通过加锁的形式进行了不同程度的隔离,下面我们了解下产生这些问题的根源,然后不同的事务隔离级别是通过什么方式解决这些问题的。
1、脏写问题
在事务并发的时候,一个事务可以修改另外一个正在进行中的事务的数据,这可能会导致一个写的事务会覆盖另外一个写的事务数据,这也就是脏写问题。 案例:以存取款为例,假设A与B同用一张银行卡,银行卡内余额为1000,如果两个事务可以对同一个数据进行修改时,可能会导致下面 事务B 把事务A的数据覆盖了,导致事务A写入的数据丢失了。 在事务隔离级别READ_UNCOMMITTED 解决了脏写的问题。 原理是在READ_UNCOMMITTED事务隔离级别下,当事务对数据进行修改时,首先会对数据加写锁,加写锁成功后只有等事务提交或者回滚后才会释放,所以已经有一个事务对数据加了写锁,那么其他事务就会因为无法获取对应数据的锁而阻塞,所以在READ_UNCOMMITTED事务隔离级别下,多个事务是无法对同一个数据同时进行修改。 |
2、脏读问题
在事务并发的时候,一个事务可以读取到另外一个正在进行中的事务数据,这产生了脏读问题。 案例: 以存取款为例,假设A与B同用一张银行卡,银行卡内余额为1000,此时事务B进行取款,事务A进行存款,如果一个事务可以读取到另外一个正在进行中的事务数据,那么可能产生下面问题。 事务A:查询银行卡余额为1000. 事务A的老婆:取款1000. 事务A的老婆:提交事务,余额为0. 事务A:查询余额,余额为0. 事务A提交事务。 不可重复读还是比较难理解的,正确的事务应该是事务A查询余额时,余额为1000。不可重复读就是事务B修改了事务A中的数据,导致事务A在同一个事务中两次读取到了不一样的数据。 在事务隔离级别READ_COMMITTED(读提交)解决了脏读的问题。 原理是在READ_COMMITTED的事务隔离级别下,当事务对数据进行修改时,得先要对数据加写锁,当事务读取数据时,首先需要对数据加读锁。 因为写锁与读锁不能共存,所以在修改数据的时候,其它事务会因为无法成功加读锁而阻塞,所以READ_COMMITTED 的事务隔离级别下,一个事务就无法读取另外一个未完成的事务所修改的数据了。 |
3、不可重复读问题
在事务并发的时候,一个事务里多次对同一个数据进行读取,但是读取到的结果是不一样的,这种问题称为不可重复读问题。 案例: 以取款为例,假设A与B同用一张银行卡,银行卡内余额为1000。 操作: 1、事务A:查询银行卡余额为1000. 2、事务B:取款1000。 3、事务B:提交事务(此时余额为0)。 4、事务A:查询余额,余额为0。 5、事务A:提交事务 当隔离级别设置为REPEATABLE_READ的事务隔离级别下,可以避免不可重复读。 不可重复读解决思路: 一个事务中的读取操作会对数据加读锁(并且在当前事务结束之前不会释放),此时另外一个事务对该数据修改之前会尝试加写锁(此时不会成功,因为读写锁冲突),所以就避免了一个事务多次读取的数据的间隔可以被另外一个事务修改。 不过实际实现的过程中,数据库解决不可重复读的方式会有所不同,在Mysql innodb引擎中,解决不可重复读的问题并不是通过加锁实现,而是通过MVCC机制实现,使用MVCC后读取数据的时候不会加读锁,而是读取的历史版本数据,在RR事务隔离级别里,MVCC保证了在一个事务里多次读取的数据历史版本是一致的,所以就无法看到最新修改的数据,这样也就保证了一个事务里多次读取到的数据肯定是一致的。 |
4、幻读问题
在事务并发的时候,一个事务可以往另外一个正在读取的事务查询范围内插入新数据,导致另外一个事务在第二次查询数据里,要比前一次查询的数据要多,同样的SQL后面一次查询凭空多出了数据,像幻觉一样所以称为幻读。 案例: 以打印银行卡账单存款记录为例,假设A与B同用一张银行卡,银行卡内存款记录为两条。 操作: 如下图所示,存款表有2条数据,A对存款表进行读取, 当A第一次读取到存款记录的时候只有2条数据; 但是与此同时B往银行卡进行了一笔存款然后提交了事务,在B提交了存款事务之后,A执行了打印操 作,最后发现打印存款表信息的时候却发现有3条数据,与之前查出的数据不一样,这种平白无故多出来 的数据就好象发生了幻觉一样所以称为幻读; |
解决幻读的两种方式
1、设置事务隔离级别为SERIALIZABLE
在SERIALIZABLE事务隔离级别下,所有的事务都串行化执行,一个事务的执行必须等前面的事务结束,这样的话查询的时候就无法有其他事务查询新的数据,所以不会产生幻读问题。
2、加间隙锁
幻读问题的本质在于,没有对查询范围内的所有数据(包括不存在的数据)进行加锁,而导致该查询范围内可以被插入新的数据,所以使用间隙锁,对查询的范围进行加锁,此时新插入的数据的事务会因为无法加锁成功而阻塞,所以就避免了幻读。比如表数据如下图, 那么此时如果执行select * from user where id>2 时 ,间隙锁会对id>2的空间加锁,所以此时我们另外一个事务插入ID为3 、4、6、7… 都会因为锁阻塞而无法成功。
二、Spring事务传播行为
1、什么是传播行为
指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。
例如:methodA事务方法调用methodB事务方法时,methodB是继续在调用者methodA的事务中运行呢,还是为自己开启一个新事务运行,这就是由methodB的事务传播行为决定的
2、Spring七种传播行为(大白话解说)
传播行为简介 : 事务传播行为用来描述由某一个事务传播行为修饰的方法被嵌套进另一个方法的时事务如何传播。 |
Spring在TransactionDefinition接口中规定了7种类型的事务传播行为。
事务传播行为是Spring框架独有的事务增强特性,它不属于事务实际提供方数据库行为。 这是Spring为我们提供的强大的工具箱,使用事务传播行可以为我们的开发工作提供许多便利。
1、PROPAGATION_REQUIRED(默认的事务传播行为)
1、 执行methodC方法,methodA,methodB均插入成功。因为methodC方法无事务,methodA,methodB均在自己的事务中运行。
2、执行methodD方法,methodA插入成功,methodExcB插入失败。因为methodC方法无事务,methodA,methodExcB均在自己的事务中运行。
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
// 插入操作
}
@Transactional(propagation = Propagation.REQUIRED)
public void methodB() {
// 插入操作
}
//该方法抛异常
@Transactional(propagation = Propagation.REQUIRED)
public void methodExcB() {
// 插入操作
throw new RuntimeException();
}
//该方法无事务
public void methodC() {
methodA();
methodB();
throw new RuntimeException();
}
public void methodD() {
methodA();
methodExcB();
}
1、 执行methodC方法,methodA,methodB均插入失败。因为methodC有事务,methodA,methodB均在methodC事务中运行,而methodC又抛异常回滚。
2、执行methodD方法,methodA,methodExcB均插入失败。因为methodD有事务,methodA,methodExcB均在methodD事务中运行。methodA或methodExcB方法抛异常,methodD方法感知到异常回滚。
3、执行methodE方法,methodA,methodExcB均插入失败。因为methodD有事务,methodA,methodExcB均在methodD事务中运行。即使methodA或methodExcB方法抛出的异常被catch ,methodD方法依然感知到异常回滚。
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
// 插入操作
}
@Transactional(propagation = Propagation.REQUIRED)
public void methodB() {
// 插入操作
}
//该方法抛异常
@Transactional(propagation = Propagation.REQUIRED)
public void methodExcB() {
// 插入操作
throw new RuntimeException();
}
//该方法无事务
@Transactional(propagation = Propagation.REQUIRED)
public void methodC() {
methodA();
methodB();
throw new RuntimeException();
}
@Transactional(propagation = Propagation.REQUIRED)
public void methodD() {
methodA();
methodExcB();
}
@Transactional(propagation = Propagation.REQUIRED)
public void methodE() {
methodA();
try {
methodExcB();
} catch (Exception e) {
System.out.println("方法回滚");
}
}
本文中只介绍PROPAGATION_REQUIRED,因为这是Spring默认的传播行为,也是最常用的,另外几个基本很少用到。如果有兴趣可以参考Spring传播行为详解