史上最清晰易懂的交易隔离级别图表说明
在网上看了很多关于事务隔离级别的文章,很多文章写的让读者看完后晕头转向、迷迷糊糊甚至一度怀疑自己的理解能力是不是很差,今天这篇文章就以非常清晰明了的思路和讲解方式打开你对事务隔离级别的全新认知!
一. 事务特征(ACID)
首先我们先列举总结一下事务中常常提到的ACID,不做展开讲解。
A:原子性(Atomicity)
原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
C:一致性(Consistency)
事务前后数据的完整性必须保持一致。
I:隔离性(Isolation)
事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。
D:持久性(Durability)
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响
我们今天讲到的事务隔离级别就是针对ACID中的隔离性来说的,如上面对隔离性进行总结的一样很多书籍在介绍隔离性的时候会提到事务之间不能相互干扰,需要相互隔离,但实际为了性能考虑各种数据库会提供不同级别的隔离性以满足不同的场景需要。
二.并发事务会产生的问题
2.1 丢失更新
2.1.1 第一类丢失更新
现象描述:A事务撤销时,把已经提交的B事务的更新数据覆盖了。
时间点 | 事务A | 事务B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账户余额为1000元 | |
T4 | 查询账户余额为1000元 | |
T5 | 存入100元把余额改为1100元 | |
T6 | 提交事务 | |
T7 | 取出100元把余额改为900元 | |
T8 | 撤销事务 | |
T9 | 余额恢复为1000元(丢失更新) |
这就是我们常提到的第一类丢失更新问题:事务B虽然成功了,但是它所做的更新没有被永久存储,这种并发问题是由于完全没有隔离事务造成的。当两个事务更新相同的数据时,如果一个事务被提交,另一个事务却撤销,那么会连同第一个事务所做的更新也被撤销了。(这是绝对避免出现的事情) 事务A的开始时间和结束时间包含事务B的开始和结束时间,事务A回滚事务的同时,把B的已经提交的事务也回滚的,这是避免的,这就是第一类丢失更新。
2.1.2 第二类丢失更新
现象描述:A事务提交时,把已经提交的B事务的更新数据覆盖了。
时间点 | 事务A | 事务B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账户余额为1000元 | |
T4 | 查询账户余额为1000元 | |
T5 | 存入100元把余额改为1100元 | |
T6 | 提交事务 | |
T7 | 取出100元把余额改为900元 | |
T8 | 提交事务 | |
T9 | 余额变为900元(丢失更新) |
第二类丢失更新和第一类的区别实际上是对数据的影响是由A事务的撤销还是提交造成的,它和不可重复读(下面介绍)本质上是同一类并发问题,通常把它看做是不可重复读的一个特例。两个或多个事务查询同一数据。然后都基于自己的查询结果更新数据,这时会造成最后一个提交的更新事务,将覆盖其它已经提交的更新事务。
2.2 脏读
现象描述:读到未提交更新的数据
时间点 | 事务A | 事务B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账户余额为1000元 | |
T4 | 取出500元把余额改为500元 | |
T5 | 查询账户余额为500元(脏读) | |
T6 | 撤销事务,余额恢复为1000元 | |
T7 | 存入100元把余额改为600元 | |
T8 | 提交事务 |
A事务查询到了B事务未提交的更新数据,A事务依据这个查询结果继续执行相关操作。但是接着B事务撤销了所做的更新,这会导致A事务操作的是脏数据,以上的示例中T5时刻产生了脏读,最终导致A事务提交时账户余额的不正确,可能有人会有疑问,B事务还没有提交或撤销,T5时刻A事务为什么能读到已经改变的数据,这里要说的是,数据表中的数据是实时改变的,事务只是控制数据的最终状态,也就是说如果没有正确的隔离级别,在更新操作语句结束后,即使事务未完成,其他事务就已经可以读取到改变的数据值了。
解决方案:
READ COMMITTED(读已提交数据)
REPEATABLE READ(可重复读)
SERIALIZABLE(串行化)
该问题只存在理论上,实际是不会发生的,因为没有数据库会允许这样的并发问题默认存在
2.3 不可重复读
现象描述:读到已经提交更新的数据,但一个事务范围内两个相同的查询却返回了不同数据。
时间点 | 事务A | 事务B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账户余额为1000元 | |
T4 | 查询账户余额为1000元 | |
T5 | 取出100元把余额改为900元 | |
T6 | 提交事务 | |
T7 | 查询账户余额为900元(与T4读取的一不一致,不可重复读) |
这是由于查询时系统中其他事务修改的提交而引起的。比如事务T1读取某一数据,事务T2读取并修改了该数据,T1为了对读取值进行检验而再次读取该数据,便得到了不同的结果。
一种更易理解的说法是:在一个事务内,多次读同一个数据。在这个事务还没有结束时,另一个事务也访问该同一数据。那么,在第一个事务的两次读数据之间。由于第二个事务的修改,那么第一个事务读到的数据可能不一样,这样就发生了在一个事务内两次读到的数据是不一样的,因此称为不可重复读,即原始读取不可重复。
解决方案:
REPEATABLE READ(可重复读)
SERIALIZABLE(串行化)
2.4 幻读
现象描述:读到已提交插入数据,幻读与不可重复读类似,幻读是查询到了另一个事务已提交的新插入数据,而不可重复读是查询到了另一个事务已提交的更新数据。
时间点 | 事务A | 事务B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 统计用户小明总存款数为1000元 | |
T4 | 新增小明的一个存款账号,存款100元 | |
T5 | 提交事务 | |
T6 | 再次统计用户小明总存款数为1100元(与T3读取的一不一致,幻读) |
A事务第一次查询时,没有问题,第二次查询时查到了B事务已提交的新插入数据,这导致两次查询结果不同。
幻读是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,比如这种修改涉及到表中的“全部数据行”。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入“一行新数据”。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样.一般解决幻读的方法是增加范围锁RangeS,锁定检锁范围为只读,这样就避免了幻读。
不可重复读和幻读的区别:
简单来说,不可重复读是由于数据修改引起的,幻读是由数据插入或者删除引起的。
解决方案:
SERIALIZABLE(串行化)
三.事务隔离级别
以上就是数据库并发事务导致的五大问题,总结来说其中两类是更新问题,三类是读问题,数据库是如何避免这种并发事务问题的呢?答案就是通过不同的事务隔离级别,在不同的隔离级别下,并发事务读取数据的结果是不一样的,比如在脏读小节里介绍的,如果是在REPEATABLE-READ隔离级别下,A事务在T5时刻读取是读取不到B事务未提交的数据的。我们需要根据业务的要求,设置不同的隔离级别,在效率和数据安全性中找到平衡点。
SQL标准定义了4类隔离级别,包括了一些具体规则,用来限定事务内外的哪些改变是可见的,哪些是不可见的。低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。
3.1 READ UNCOMMITTED(读未提交数据)
当数据库系统使用READ UNCOMMITTED隔离级别时,一个事务在执行过程中可以看到其他事务没有提交的新插入的记录,而且还能看到其他事务没有提交的对已有记录的更新。
该事务级别只有理论分析意义,没有任何实际意义,因为没有任何一个真实的系统会设置这种隔离级别!
3.2 READ COMMITTED(读已提交数据)
当数据库系统使用READ COMMITTED隔离级别时,一个事务在执行过程中可以看到其他事务已经提交的新插入的记录,而且还能看到其他事务已经提交的对已有记录的更新。
3.3 REPEATABLE READ(可重复读)
当数据库系统使用REPEATABLE READ隔离级别时,一个事务在执行过程中可以看到其他事务已经提交的新插入的记录,但是不能看到其他事务对已有记录的更新。
3.4 SERIALIZABLE(串行化)
当数据库系统使用SERIALIZABLE隔离级别时,一个事务在执行过程中完全看不到其他事务对数据库所做的更新。当两个事务同时操作数据库中相同数据时,如果第一个事务已经在访问该数据,第二个事务只能停下来等待,必须等到第一个事务结束后才能恢复运行。因此这两个事务实际上是串行化方式运行。
以上的四种隔离级别按从低到高排序,你可能会说,选择SERIALIZABLE,因为它最安全!没错,它是最安全,但它也是最慢的!四种隔离级别的安全性与性能成反比!最安全的性能最差,最不安全的性能最好!
四.隔离级别与并发问题
OK,如果你上面的都已经理解了,那么你只需要收藏一下下面的这张表格即可,以方便后续理解或者面试时需要。
隔离级别 | 第一类丢失更新 | 第二类丢失更新 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|---|
READ UNCOMMITTED(读未提交) | 避免 | 允许 | 允许 | 允许 | 允许 |
READ COMMITTED (读已提交) | 避免 | 允许 | 避免 | 允许 | 允许 |
REPEATABLE READ(可重复读) | 避免 | 避免 | 避免 | 避免 | 允许 |
SERIALIZABLE (串行化) | 避免 | 避免 | 避免 | 避免 | 避免 |
大多数关系数据库默认使用Read committed的隔离级别,Mysql InnoDB默认使用Read repeatable的隔离级别,这和Mysql replication 机制使用Statement日志格式有关。各数据库隔离级别的实现也是有差别的,例如Oracle支持Read committed 和Serializable两种隔离级别,另外可以通过使用读快照在Read committed级别上禁止不可重复读问题;MySQL默认采用RR隔离级别,SQL标准是要求RR解决不可重复读的问题,但是因为MySQL采用了gap lock,所以实际上MySQL的RR隔离级别也解决了幻读的问题,也就是Mysql InnoDB在Read repeatable级别上使用next-key locking 策略来避免幻读现象的产生。
比如:如果我T1事物开启后,先查询主键id=1的记录,没查询到,准备开始插入。此时T2事物开启,直接插入了id=1的记录,提交。接着T1事物继续,再次查询id=1的记录的时候就会发现存在id=1的记录,这样就是幻读的场景,T1事物在执行过程中可以看到T2事物新插入的记录。另外,如果是mysql在可重复读隔离级别下的情况下,T1再次查询id=1时候,是查不到T2提交的数据的,mysql用MVCC自动帮我们解决了幻读,但是如果T1继续插入id=1的数据,也会报主键冲突,所以只是达到了读取不到的目的。具体可以看看mysql的加锁机制和mvcc。
OK,关于事务隔离级别的这篇文章就到这了,如果大家有不清楚的地方欢迎给我留言,一起讨论!
上一篇: 5 分钟读懂事务隔离和隔离级别
下一篇: 事务的隔离级别
推荐阅读