浅谈数据库隔离级别

62 阅读5分钟

我们在展开隔离级别前先探讨一下 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 来达到效果,而非真正的串行执行。