图解MySQL的四种事务隔离级别与实现原理

420 阅读8分钟

事务的几个相关概念

什么是事务?

事务(Transaction),一般是指要做的或所做的事情。在计算机术语中是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。

为什么需要事务?
技术是服务于业务的,在许多使用数据库的业务场景中,经常需要进行一系列操作,这一系列操作要么全都成功,要么全都失败,不允许出现成功了一半这样的中间情况。(例如经典银行转账案例,A扣10块钱,B加十块钱,要么全部执行成功完成转账,要么A扣的钱回滚回去,B也不会加钱)

事务的ACID特性

基于对业务的总结,人们总结出了事务应该具有如下四个特性,即:

  • Atomic-原子性,原子在化学概念上是指化学反应不可再分的基本微粒,顾名思义原子性即要么都成功,要么都失败。
  • Consistency-一致性,其实和原子性意思差不多,事务必须是使数据库从一个一致性状态变到另一个一致性状态。举例子来说:A有100块钱,B有50块钱,A和B总共有150块钱这是一个一致性状态;转账之后,A有90块钱,B有60块钱,A和B还是一共150块钱。
  • Isolation-隔离性,是指在并发多个事务访问的情况下,并发的事务是相互隔离的,一个事务的执行不能够被其它事务干扰。本文要讨论的MySQL的四种事务隔离级别,就是为了保证并发度的前提下,你的事务到底能接受多大程度的不被干扰
  • Duration-持久性,保证了上述各种特性之后,业务上还需要这个数据库保证持久性,不能出现我转账都完成了/我炉石开出金色传说了,结果数据库断电了,重启后回到解放前了。这种属于不可接受的线上事故,一般数据库都会有各种手段在尽可能保证性能的同时持久化写数据到磁盘上。

MVCC

当前读 vs. 快照读

当前读,每次都读取记录的最新版本,并且会对记录进行加锁,典型的当前读操作:

  • select lock in share mode(共享锁)
  • select for update(排他锁)
  • update(排他锁)
  • insert(排他锁)
  • delete(排他锁) 快照读,每次读取操作读到的实际是基于当前可见性生成的快照,快照的实现基于多版本并发控制(MVCC),我们日常使用的不加锁的select就是一种快照读(当事务隔离级别退化为串行时,默认select就是当前读)。

快照读是为了解决上文中提到的事务ACID特性中的Isolation隔离性而诞生的,有了快照的存在,会让每个事务只看到自己应该看到,仿佛数据库系统只有当前一个事务在执行一样,正是隔离性的体现。

MVCC实现原理-ReadView

MySQl实现快照读的原理就是MVCC(Multi-Version Concurrency Control,多版本并发控制),而MVCC的核心就是快照ReadView,根据设置的不同的隔离级别(RC/RR)在不同的时机拍一张当前能看到记录的快照,这张照片就是ReadView。

拍个照纪念下

假设当前有一张student表,数据内容如下:

idname
1Alice

那么在实际的数据库存储中,mysql会有隐藏的几行:

  • DB_TRX_ID
    该行记录的最近修改过的事务id,就像是文件系统里的最近修改时间一样,是生成ReadView时可见性判断的重要依据
  • DB_ROLL_PTR
    回滚指针,如果这个记录被修改过,那么会指向上一个版本,形成了一个历史版本的链表
  • DB_ROW_ID
    隐藏主键,当我们的表没有指定主键的时候,这个字段就会作为聚簇索引

所以这个student表实际在db中存储的格式可能为

student表的实际存储格式

上图表明当前这个记录是 1:Bob,最近修改的事务id是4,隐藏主键也是1。
这个记录历史上最早name是Cao(事务6创建的),
之后被事务1修改为Bob,
最后被事务4修改为Alica

基于ReadView的快照读

依据当前的事务隔离级别,MySQL事务会在某个时机下生成一个ReadView(开始拍照片),基于上面理解,我们知道,一行记录会有DB_ROLL_PTR(回滚指针)拉起来的一个链表,指向该行记录的历史版本。在快照读的时候,会基于DB_TRX_ID判断 这行记录是否可见,从而完成查询,整个过程如下图。

快照读流程

ReadView可见性算法

如何判断这行记录是否可见?ReadView是通过维护了这几个值来判断的:

  • trx_list
    快照生成时还在活跃的事务id(活跃指的是处于 begin -> dosomething -> commit的dosomething阶段,尚未commit的事务)
  • up_limit_id
    记录trx_list中最小的事务id,小于这个事务id的事务必然都已经提交过了,可以理解为是历史记录,肯定可见
  • low_limit_id
    记录ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1,这个id之后都是快照生成之后操作,肯定不可见

基于以上三个属性,可以将student表中各个记录以及其undo log中的回滚版本记录的DB_TRX_ID归类为如下。

可见性

基于以上信息,可以得到MVCC判断某行记录是否可见的伪代码如下:

def is_visible(row):
    if row.DB_TRX_ID < up_limit_id:
        return true # 1. 操作事务是历史事务了,肯定可见
    if row.DB_TRX_ID > low_limit_id:
        return false # 2. 操作事务是一个晚于当前ReadView的事务,肯定看不到
    if row.DB_TRX_ID in trx_list:
        return false # 3. 在活跃事务中(说明尚未提交),不可见
    return true

ACID中的隔离性与并发性的讨论

四种隔离级别总结

MySQL四种隔离级别总结如下:

隔离级别名称的含义可能有的问题
READ UNCOMMITTED这个模式下,一个事务能读(read)到另一个事务还没提交的(uncommitted)更改脏读,另一个事务还没提交的修改,这种不完整的数据称为脏数据
READ COMMITTED这个模式下,一个事务能读(read)到另一个事务已经提交的(committed)更改不可重复读,因为如果重复读的话就会看到结果变了(灵异事件,害怕.jpg)
REPEATABLE READ(MySQL默认这个模式下,一个事务能重复读(read)同样的记录保证结果相同(不管另一个事务提交还没提交)(可以简单理解为做了一个这个事务id为key的缓存)幻读,在一个事务中,第一次查询某条记录,发现没有,但是,当试图更新这条不存在的记录时,竟然能成功,并且,再次读取同一条记录,它就神奇地出现了(害怕.jpg)
SERIALIZABLE完全串行,一个事务开启后,另一个事务被挂起无任何问题,就是没法并发了,性能很差。幻读出现的条件很苛刻,而且一般来说事务中不会有人更新一个“不存在的记录”,所以一般RR隔离级别足够

READ UNCOMMITTED

基于上文对ReadView的理解,我们可以从原理上理解各个隔离级别是怎么做到的。RU级别,即没有MVCC机制的情况下的方式,即每次select都是获取到某行记录最新的结果。

READ UNCOMMITTED

READ COMMITTED

从RC隔离级别开始,我们开始使用MVCC机制了,在每次读操作的时候都会建立建立一次ReadView,这个ReadView自然会隐藏掉本事务不该见到的记录。

READ COMMITED

REPEATABLE READ(默认隔离级别)

从上面的分析可以看出,每次读操作都建立一次ReadView确实可以保证 在活跃事务中(说明尚未提交)的记录不可见,但是每次读都生成新的ReadView展示当前最新的可见性,会导致看到其它事务已提交的修改,这又在某些场景下不符合隔离性的 仿佛没有其它事务在执行 的要求。因此又有了更严格的RR级别,这种级别下 第一次执行操作后 会生成ReadView,并且以后的查询操作都会使用这一版本的ReadView,保证了可见性始终如一。

REPEATABLE READ

如上图所示,RR级别在某种意义上来说已经是一种非常完美的隔离级别了,事务2的整个执行过程都仿佛完全感知不到事务1的存在。只是这种模式下有一种产生幻读的可能,如果此时事务1插入了一条id=2的记录,这时事务2还是看不到的,但是如果这时事务2更新了这条id=2的记录,那么我们可以发现MySQl会将这条被更新的最新结果加入到事务2当前的ReadView中,从而导致出现了幻读的现象。

SERIALIZABLE

事务的最高隔离级别,不存在任何的脏读、不可重复读、幻读的问题,事务完全按照顺序线性执行,性能极大损失,一般不会使用。

SERIALIZABLE

reference

  1. ^ 事务-百度百科
  2. ^ Repeatable Read - 廖雪峰的官方网站
  3. ^ mysql 设置隔离级别 - anobscureretreat - 博客园
  4. ^ MVCC多版本并发控制 - 简书