数据库中事务的几种隔离级别分别解决了哪些问题
前面一直在写 JVM 系列的文章,直到有一天,卡壳了,后面不知道写啥了,原因就是笔者是一个菜鸟(公众号名称就能看出),懂得少,理解也不够透彻,导致差不多快两个月没更了(主要还是因为懒)。
最近打开微信公众号后台一看,发现停更两月,居然涨了 200 多个粉(上次春节也是因为懒,停更了快 3 个月,掉了 400 多个粉),心里一喜,决定还是继续坚持下去(程序员的快乐就是这么简单),不过 JVM 系列先暂停一段时间,而是接着之前分享过的 MySQL 系列继续(不出意外,接下来几篇将主要分享 MySQL 系列和 Redis 系列的知识)。
先来篇基础一点的文章,找找感觉,今天讲讲事务的隔离级别,虽然很基础,但是面试时却经常被问到(初级攻城狮),所以还是很有必要了解一下,而且后面计划分享的两篇文章,和事务的隔离级别息息相关。
4 种现象与 4 种隔离级别
众所周知,通常关系型数据库在高并发面前,很弱鸡,但是吧,它即使再弱鸡,在正常的使用过程中,也存在一些并发的场景。而一出现并发,就会出现各种各样的问题,例如:脏写、脏读、不可重复读、幻读,这都是些啥呢?下面一一具体举例解释。
脏写现象与读未提交(read uncommitted)隔离级别
事务 A 和事务 B 同时执行,它们都要对同一条数据进行修改(这条数据的值,假设为 data)。
1. 首先是事务 A 将数据的值修改为 data1,暂时不提交事务;
2. 然后事务 B 将数据的值修改为 data2,然后立马提交事务;
3. 事务 A 可能由于自己的业务系统出现了异常,因此进行回滚操作,将数据的值重新回滚为 data。
在这个过程中,事务 A 一个回滚操作,将事务 B 修改的值也回滚了,一夜回到解放前,事务 B 白忙活一场,这种现象叫做脏写。它的本质就是一个事务将另一个事务提交的修改操作回滚了。
显然在数据库中,肯定不允许这种现象存在。那么该如何解决这种问题呢?加个写锁就能解决,要对数据修改,必须要先获取到这行数据的写锁,否则不能修改。
在事务 A 开启时,对 data 加上写锁,直到事务 A 提交事务以后,才释放锁,在此期间,其他事务由于获取不到锁,也就谈不上对 data 数据进行修改了。
在实际的数据库中,则是将事务的隔离级别设置为读未提交(read uncommitted) ,在该隔离级别下,能保证事务提交之前,其他事务不能同时对这条数据进行修改。
脏读现象与读提交(read committed)隔离级别
事务 A 和事务 B 同时执行,事务 A 先将数据从 data 修改为 data1,然后暂时不提交事务,而是继续向后处理业务逻辑。
然后事务 B 读取这一行数据,读取到值为 data1,然后基于 data1 这个值去处理自己的业务逻辑了。
接着事务 A 在处理后面的业务逻辑时出现了异常,因此要进行回滚操作,将数据从 data1 回滚为 data。
在这个过程中,当事务 B 再去查询时发现数据的值为 data,这就蛋疼了,本来是基于 data1 这个值去做的业务逻辑处理,结果现在发现值却是 data,完蛋了,全 NM 错了,这种现象就是脏读,它的本质就是一个事务读到了另一个事务未提交的值。
为了解决脏读的问题,数据库中定义了读提交(read committed) 隔离级别,它的意思就是,在读数据的时候,只能读到别的事务提交过后的值,对于未提交的事务对数据所做的修改操作,当前事务是无法读取到的。
在读提交的事务隔离级别下,当事务 B 去读取数据时,发现事务 A 还没有提交,因此它不能读取到 data1 这个值,只能读取到 data 这个值。
不可重复读现象与可重复读(repeatable read)隔离级别
假设现在数据库中事务的隔离级别为读提交,也就是未提交的事务修改的值,其他事务是读取不到的,那么在当前事务隔离级别下还会有其他问题吗?
事务 A 和事务 B 同时开启事务,事务 A 先从数据库查询数据,读取到的值为 data,然后事务 A 先不提交事务。
接着事务 B 修改数据,将数据的值从 data 修改为 data1。
如果事务 B 先不提交事务,那么事务 A 此时来读取数据时,能读取到最新的值 data1 吗?不能,因为我们假设了此时事务的隔离级别处于读提交状态。
好,既然不能事务 A 不能读取到最新值,那么现在事务 B 提交事务,接着让事务 A 再次从数据库查询数据,此时能读取到最新的值吗?
能,此时读取到的值为 data1,因为事务 B 已经提交了事务,在读提交的隔离级别下,提交了的事务,其他事务都能读取到最新的值。
但是这有问题啊!在同一个事务内(事务 A),它读取了两次数据,发现前后两次读取到的值分别是 data 和 data1,同一行数据,读到的值却不一样,事务 A 此时心里可能 MMP 了,干啥啊,忽悠我呢!
实际上这就是不可重复读现象,它的本质就是在同一个事务内,多次从数据库读取数据,读取到的值不一样。(注意不可重复读与脏读的区别:脏读是指读到了未提交事务的值,不可重复读指的是其他事务更新数据并提交后,自己前后读取到的数据不一致)
因此,可重复读(repeatable read) 事务隔离级别出现了,它的意思是,在同一个事务内,例如事务 A,多次从数据库读取数据时,每网站监控次读取到的值是一样的,即使在此期间有其他事务修改了这条数据的值,也不会导致事务 A 前后两次读取到的值不一样。
幻读现象与串行化(serializable)隔离级别
假设事务 A 和事务 B 并发执行,首先事务 A 先执行了如下 SQL,假设查到了 1 条数据;
### 假设只查询出来一条数据
select * from t where id > 1
接着事务 B 向数据库中又新插入了 10 条数据,并提交了事务;
然后事务 A 又使用同样的 SQL 语句查询数据,这时会查询出来 11 条数据,比之前查出来的数据多,也就是说看到了更多的数据,这种现象就是幻读。
注意幻读与不可重复读的区别:幻读特指在同一个事务内,前后两次查询,后面的查询,读到了之前没看到的数据;而不可重复读指的是在同一个事务内,针对同一行数据而言,前后两次查询,读取到的值不一样。
为了解决幻读的问题,数据库提出了串行化(Serializable) 这种事务隔离级别。
那么什么是串行化呢?归根结底,出现脏写、脏读、不可重复读、幻读这些问题,都是因为并发导致的,那要一下子全部解决这些问题,最简单的办法就是不要让线程并发执行,让多个线程一个一个执行,也就是串行化(也就是不让并发出现,都没有并发了,也就没有脏写、脏读、不可重复读、幻读这些幺蛾子了)。
总结
由于事务的并发执行,会造成很多异常的现象,例如脏写、脏读、不可重复读、幻读等。
这四种现象总结起来就是:
1. 脏写指的是一个事务将其他事务提交的修改回滚了;
2. 脏读指的是一个事务读取到了另一个事务未提交的修改值;
3. 不可重复读指的是一个事务对同一条数据,两次前后读取到的值不一样,这是因为在此期间有其他事务更新了该条数据;
4. 幻读指的是一个事务,后一次的查询比前一次查询看到的数据多了,它特指读到了新的数据,需要与不可重复读的现象区分开来。
为了解决这些问题,SQL 标准(注意:这里说的是 SQL 标准)中定义了 4 种事务的隔离级来应对这些现象,分别是:读未提交、读提交、可重复读、串行化,它们的强度也依次递增。在这四种隔离级别下,它们的表现如下:
实际上,这四种事务的隔离级别只是 SQL 标准中定义的,在各大数据库中,这 4 种隔离级别在实现细节上又有所不同,例如:对于 MySQL 的 InnoDB 存储引擎而言,在可重复读隔离级别下,MySQL 通过 MVCC 机制解决了幻读的问题(在 SQL 标准中,可重复读隔离级别下仍存在幻读的问题)。
那么 MySQL 是如何通过 MVCC 机制解决幻读的,下篇文章 《MySQL 的可重复读隔离级别下还存在幻读的问题吗?MVCC 机制的实现原理》 分析(立个 flag,这篇文章这周日发)。