一、简介
经常有面试官提出这么一个问题:什么是脏读、不可重复读和幻读?
关于这个问题,我们还得从数据库的管理系统说起,当数据库管理系统在写入或者更新数据的过程中,为了保证数据是正确可靠的,需要满足四个特性:原子性、一致性、隔离性和持久性,简称 ACID !
- Atomicity(原子性):一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,能被恢复(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
- Consistency(一致性):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入之前和写入之后的数据必须完全符合预期设定的结果。
- Isolation(隔离性):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
- Durability(持久性):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
例如以银行转账为例,从原账户扣除金额,以及向目标账户添加金额,这两个阶段的操作,被视为一个完整的逻辑过程,不可拆分,简单的说,要么全部成功,要么全部失败!这个过程被称为一个事务,具有 ACID 四个特点!
说了这么多,跟我们今天要说的脏读、不可重复读和幻读有什么关系呢?
我们都知道,当下主流的数据库,都支持多个事务并发执行,当一个事务在写入数据,另一个事务也要读这条数据,会出现哪些问题?当一个事务在写入数据,另一个事务也要写入这条数据,又会发生什么哪些问题?
当多个事务并发处理同一条数据时,如果事务隔离性不合理,就会产生我们今天要介绍的内容,具体的说就是:脏读、不可重复读和幻读!
在事务的四个特性里面,其中隔离性总共分为四种级别:由低到高依次为 Read uncommitted 、Read committed 、Repeatable read 、Serializable ,这四个级别可以逐个解决脏读 、不可重复读 、幻读等这几类问题。
- read uncommitted:俗称读未提交,指的是一个事务还没提交时,它做的变更就能被别的事务看到。
- Read committed:俗称读提交,指的是一个事务提交之后,它做的变更才会被其他事务看到。
- Repeatable read:俗称可重复读,指的是一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的,同时当其他事务在未提交时,变更是不可见的。
- Serializable:俗称串行化,顾名思义就是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
不同的隔离级别,产生的结果是不一样,下面我们一起来具体分析分析!
二、场景分析
2.1、脏读
所谓的脏读,指的是读到了其他事务未提交的数据,未提交意味着这些数据可能会保存到数据库,也可能会回滚,不保存到数据库。当这个数据发生了回滚,就意味着这个数据不存在,这就是脏读!
脏读最大的问题就是可能会读到不存在的数据。比如在上图中,事务 B 的更新数据被事务 A 读取,但是事务 B 回滚了,更新数据全部还原。也就是说事务 A 刚刚读到的数据并没有存在于数据库中。
从结果上看,事务 A 读出了一条不存在的数据,这个问题比较很严重!
当数据库的事务隔离级别为读未提交,就会发生脏读现象!
2.2、不可重复读
不可重复读,指的是在一个事务内,最开始读到的数据和事务结束前的任意时刻读到的同一批数据出现不一致的情况。
比如上图,事务 A 两次读取同一数据,第一次读取结果为 1,当事务 B 修改了数据并提交,此时的事务 A 第二次读取结果为 2,两次读取结果不一致!
当数据库的事务隔离级别为读未提交、读提交时,就会发生不可重复读现象!
2.3、幻读
幻读和不可重复读,有点类似,但是表达的侧重点不一样。
例如事务 A 对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。此时,突然事务 B 插入了一条数据并提交了,当事务 A 提交了修改数据操作之后,再次读取全部数据,结果发现还有一条数据未更新,给人感觉好像产生了幻觉一样。这就是幻读!
当有别的事务,在插入或者删除同一条数据的时候,就容易产生幻读的现象!
当数据库的事务隔离级别为读未提交、读提交、可重复读时,就会发生幻读现象!
三、如何解决
3.1 脏读
在事务隔离级别READ_COMMITTED 解决了脏写的问题,其原理是在READ_COMMITTED事务隔离级别下,当事务对数据进行修改时,首先会对数据加写锁,加写锁成功后只有等事务提交或者回滚后才会释放,所以已经有一个事务对数据加了写锁,那么其他事务就会因为无法获取对应数据的锁而阻塞,所以在READ_COMMITTED事务隔离级别下,多个事务是无法对同一个数据同时进行修改。
3.2 不可重复读
不可重复读产生的核心问题是,在一个事务第1次读取和第2次读取数据的间隔过程中可以被另外一个事务修改,因为在READ_COMMITTED的事务隔离级别下,事务中每次读取数据结束后(事务未结束)就会释放读锁,而一旦读锁释放后另外一个事务就可以加写锁,最终导致事务中多次读取该数据的间隙中可以被其它事务修改。
而REPEATABLE_READ (可重复读)的事务隔离级别下,一个事务中的读取操作会对数据加读锁(并且在当前事务结束之前不会释放),此时另外一个事务对该数据修改之前会尝试加写锁(此时不会成功,因为读写锁冲突),所以就避免了一个事务多次读取的数据的间隔可以被另外一个事务修改。
不过实际实现的过程中,数据库解决不可重复读的方式会有所不同,在Mysql innodb引擎中,解决不可重复读的问题并不是通过加锁实现,而是通过MVCC机制实现,使用MVCC后读取数据的时候不会加读锁,而是读取的历史版本数据,在RR事务隔离级别里,MVCC保证了在一个事务里多次读取的数据历史版本是一致的,所以就无法看到最新修改的数据,这样也就保证了一个事务里多次读取到的数据肯定是一致的。
3.3 幻读
1、设置事务隔离级别为SERIALIZABLE
在SERIALIZABLE事务隔离级别下,所有的事务都串行化执行,一个事务的执行必须等前面的事务结束,这样的话查询的时候就无法有其他事务查询新的数据,所以不会产生幻读问题。
2、加间隙锁
幻读问题的本质在于,没有对查询范围内的所有数据(包括不存在的数据)进行加锁,而导致该查询范围内可以被插入新的数据,所以使用间隙锁,对查询的范围进行加锁,此时新插入的数据的事务会因为无法加锁成功而阻塞,所以就避免了幻读。比如表数据如下图, 那么此时如果执行select * from user where id>2 时 ,间隙锁会对id>2的空间加锁,所以此时我们另外一个事务插入ID为3 、4、6、7....... 都会因为锁阻塞而无法成功。
3、加Next-key锁
四、幻读和不可重复读的区别
脏读:事务A正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务B也访问这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么事务B读到的这个数据是脏数据,依据脏数据所做的操作可能是不正确的。【事务B读取到了事务A没有提交的数据】
不可重复读:事务A在执行读取操作,由整个事务A比较大,前后读取同一条数据需要经历很长的时间 。在事务A第一次读取数据后,事务B执行了更改操作,事务A第二次读取到该值时发现和之前的数据不一样,系统不可以读取到重复的数据,称为不可重复读。【同一个事务中重复读取时获得的数据不同】
幻读:事务A在执行读取操作,需要两次统计数据的总量,前一次查询数据总量后,此时事务B执行了新增数据的操作并提交后,而后事务A第二次读取的数据总量和之前统计的不一样,就像产生了幻觉一样,平白无故的多了几条数据,称为幻读。【前后多次读取,数据总量不一致】
不可重复读的重点在修改,在同一事务中,同样的条件,第一次读取的数据和第二次读取的数据不一样(因为中间有其他事务提交了修改) 幻读的重点在删除和插入,在同一事务中,同样的条件,第一次读取的记录数第二次读取的记录数不一致。【因为中间有其他事务提交了插入和删除操作】
不可重复读关键在于特定的某个数据发生了变化,而幻读关键在于整体的数据量发生了变化。