我们在展开隔离级别前先探讨一下 Why,就是为什么需要隔离级别?隔离级别解决什么问题?
答案就是解决并发问题!事务的执行是并发且不可控,你无法保证哪个事务先执行、哪段 SQL 先执行,这就导致事务普遍存在的三个并发问题:脏读、不可重复读以及幻读。
读未提交
这个隔离级别可以说是完全没隔离,隔离性最低,三个并发问题同时存在。见名知义,一个事务可以读取另一个事务未提交的数据!。这里我们先探讨什么是脏读?如以下表格所示:
| 步骤 | 事务 A | 事务 B |
|---|---|---|
| 1 | 开始事务 | |
| 2 | 开始事务 | |
| 3 | 修改字段:火影忍者 -> 海贼王 | |
| 4 | 读取:海贼王 | |
| 5 | 事务回滚:海贼王 -> 火影忍者 | |
| 6 | 结束事务 | 结束事务 |
| 最终结果 | 数据库内的数据还是火影忍者,但是事务 B 读到的数据是海贼王,这就是一条脏数据 |
当 A 事务修改了字段的值为 ‘海贼王’,B 事务就可以读到这个值,但是可能出于某些异常,事务 A 回滚了,那么数据库中的最终值还是‘火影忍者’,但是事务 B 读到的值依旧是‘海贼王’,这个值就是脏数据,所以叫脏读。
那如何解决脏读问题呢?很简单,让事务只读取已提交的数据不就好了,所以下一个隔离级别叫读已提交。
读已提交
在这个隔离级别里,事务只允许读已经提交的值,也就是持久化到磁盘的数据,我们解决了脏读的问题,保证事务读取的值永远是磁盘上持久化的,但是不可重复读和幻读依旧存在。
这里我们讨论不可重复读,其实这个翻译一直让我感觉很糟糕,让我不是很明白怎么就不能重复读呢?是二次读取会报错吗?其实都不是。下面我来解释一下什么是不可重复读,看下表所示:
| 步骤 | 事务 A | 事务 B |
|---|---|---|
| 1 | 开始事务 | |
| 2 | 开始事务 | 读取:火影忍者 |
| 3 | 修改字段:火影忍者 -> 海贼王 | |
| 4 | 提交事务 | |
| 5 | 结束事务 | 读取:海贼王 |
| 6 | 结束事务 | |
| 最终结果 | 事务 B 两次读取的值不相同 |
事务 B 读取字段的时候,读取到的值是“火影忍者”,此时事务 A 开始执行,把字段的值改为了“海贼王”。这个时候事务 A 还没有结束,当事务 A 再次读取值的时候发现跟上一次读取的值不一样,这就是不可重复读,一个事务内两次读取的值不相同。
发生的原因也很简单,在该隔离级别下,事务只能允许读已经提交的值,因为事务的执行顺序是不可控的,在一个事务结束前,完全可以有其他事务完成修改提交动作。
最后还是老问题,怎么避免这个问题呢?让事务重复读取的值一样就可以了,我们来看看下一个隔离级别:可重复读。
可重复读
可重复读是数据库事务隔离级别的中的第三级,它的核心是:在一个事务内,重复读取同一条数据,得到的内容是一样的,即便其他事务修改数据并提交了。具体是通过 MVCC(多版本并发控制)实现的,这里不深入探讨,简单来讲就是每条数据都有多个版本,每个版本对应一个事务,当启动一个事务时,会有一个当前事务的数据快照,该事务后续所有的读操作都是基于该快照。
这里要说明一下,按照 SQL 标准,可重复读是不解决幻读的,但是主流数据库基本都在该隔离级别解决了幻读的问题:
- PostgreSQL:通过SSI(序列化快照隔离)解决幻读
- MySQL InnoDB:通过Next-Key Lock(间隙锁)解决幻读
- 但 Oracle 需要显式加锁才能防幻读
那什么是幻读呢?幻读其实跟不可重复读很相似,只是不可重复读是一个事务内读取的一条记录的内容不同,而幻读是一个事务内读取的多条记录的数目不同,看下表所示:
| 步骤 | 事务 A | 事务 B |
|---|---|---|
| 1 | 开始事务 | |
| 2 | 开始事务 | 读取日漫:海贼王,火影忍者,龙珠 |
| 3 | 删除日漫:火影忍者 | |
| 4 | 提交事务 | |
| 5 | 结束事务 | |
| 6 | 读取日漫:海贼王,龙珠 | |
| 最终结果 | 事务 B 两次读取的数据记录条数不同 |
当事务 A 读取一组数据时,事务 B 对表进行了新增或删除并提价了事务,当事务 A 再次读取时,会比上次少或多,这就是幻读。
串行化
这时大家可能会有疑惑,那数据库能不能解决所有的并发问题?答案是可以,但是有代价。接下来让我们认识一下最后一个隔离级别:串行化。
| 步骤 | 事务 A | 事务 B |
|---|---|---|
| 1 | 开始事务 | |
| 2 | 修改字段:火影忍者 -> 海贼王 | |
| 3 | 提交事务,结束事务 | |
| 4 | 开始事务 | |
| 5 | 读取:海贼王 | |
| 6 | 结束事务 | |
| 最终结果 | 同一个时刻只能有一个事务执行,不存在任何并发问题 |
这个隔离级别的隔离性最高,核心思想就是所有事务全都给我乖乖排队,依次一个一个执行,同一个时刻只允许一个事务执行,不存在任何并发问题。代价也显而易见,没有并发,没有性能,所以性能最差。实际上数据库很少完全串行化执行,而是通过锁机制或 SSI 来达到效果,而非真正的串行执行。