锁篇

  • 事务的隔离性由锁来实现

概念

  • 在数据库中,除传统的计算资源(如CPU、RAM、I/O等)的争用以外,数据也是一种供许多用户共享的资源。为保证数据的一致性,需要对并发操作进行控制,因此产生了锁。同时锁机制也为实现MySQL的各个隔离级别提供了保证。锁冲突也是影响数据库并发访问性能的一个重要因素。

MySQL并发事务访问相同记录

  • 并发事务访问相同记录的情况大致可以划分为3种:

读-读情况

  • 读-读情况,即并发事务相继读取相同的记录。读取操作本身不会对记录有任何影响,并不会引起什么问题,所以允许这种情况的发生

写-写情况

  • 写-写情况,即并发事务相继对相同的记录做出改动

  • 在这种情况下会发生脏写的问题,任何一种隔离级别都不允许这种问题的发生。所以在多个未提交事务相继对一条记录做改变时,需要让他们排队执行,这个排队的过程其实是通过锁来实现的。这个所谓的锁其实是一个内存中的结构,在事务执行前本来是没有所得,就是说一开始是没有锁结构和记录进行行关联的。

  • 当一个事务想对这条记录做改动时,首先会看看内存中有没有与这条记录关联的锁结构,当没有的时候就会在内存中生成一个锁结构与之关联。

  • 锁结构包含两种信息

    1. trx信息

    2. is_waiting

  • 小结几种说法:

    • 不加锁

      • 意思就是不需要在内存中生成对应的锁结构,可以直接执行操作
    • 获取锁成功,或者加锁成功

      • 意思就是在内存中生成了对应的锁结构,而且锁结构的 is_waiting 属性为 false,也就是事务可以继续执行操作
    • 获取锁失败,或者加锁失败,或者没有获取到锁

      • 意思就是在内存中生成了对应的锁结构,不过锁结构的 is_waiting 属性为 true,也就是事务需要等待,不可以继续执行操作

读-写或写-读情况

  1. 读-写或写-读,即一个事务进行读取操作,另一个事务进行改动操作。这种情况下可能发生脏读、不可重复读、幻读的问题。

  2. MySQL在REPEATABLE READ 隔离级别上就已经解决了幻读的问题

并发问题的解决方案

  • 怎么解决脏读、不可重复读、幻读这些问题呢?其实有两种可选的解决方案

    1. 读操作利用多版本并发控制(MVCC),写操作进行加锁

      • 普通的SELECT语句在READ COMMITTED 和 REPEATABLE READ 隔离级别下会使用到 MVCC 读取记录
      • 在 READ_COMMITTED 隔离级别下,一个事务在执行过程种每次执行 SELECT 操作时都会生成一个 ReadView,ReadVIew的存在本身就保证了 事务不可以读取到未提交的事务所做的更改,也就避免了脏读现象
      • 在 REPEATABLE READ隔离级别下,一个事务在执行过程种只有 第一次执行SELECT操作 才会生成一个 ReadView,之后的SELECT操作都会复用这个 ReadView,这样就避免了不可重复读和幻读的问题
    2. 读、写操作都采用加锁的方式

小结

  • 采用MVCC方式的话,读-写操作彼此并不冲突,性能更高
  • 采用加锁方式的话,读-写操作彼此需要排队执行,影响性能
  • 一般情况下采用MVCC来解决读-写操作并发执行的问题,但是业务在某些特殊情况下,要求必须采用加锁的方式执行。

锁的不同角度分类

  • 对数据的操作类型划分

    • 读锁/共享锁
    • 写锁/排他锁
  • 锁粒角度划分

    • 表级锁
      • 表记别的S锁、X锁
      • 意向锁
      • 自增锁
      • MDL锁
    • 行级锁
      • Record Locks
      • Gap Locks
      • Next-Key Locks
      • 插入意向锁
    • 页级锁
  • 对待锁的态度划分

    • 悲观锁
    • 乐观锁
  • 加锁方式

    • 隐式锁
    • 显示锁
  • 其他

    • 全局锁

    • 死锁

从数据操作的类型划分:读锁、写锁

  • 读锁:也称为共享锁、英文用 S 表示。针对同一份数据,多个事务的读操作可以同时进行而不会互相影响,相互不阻塞的
  • 写锁:也称为排他锁,英文用 X 表示,当前写操作没有完成前,它会阻断其他写锁和读锁。这样就能确保在给定的时间里,只有一个事务能执行写入,并防止其他用户读取正在写入的同一资源
  • 需要注意的是对于 InnoDB 引擎来说,读锁和写锁可以加在表上,也可以加在行上

从数据操作的粒度划分:表级锁、页级锁、行锁

  1. 表锁(TABLE Lock)

    1. 表记别的S锁、X锁

      • 在对某个表执行 SELECT、INSERT、DELETE、UPDATE语句时,InnoDB引擎是不会为这个表添加表级别的S锁或者X锁的。在对某个表执行一些诸如 ALTER TABLE、DROP TABLE 这类的 DDL 语句时,同理,某个事务种对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,在其他会话中对这个表执行 DDL 语句也会发生阻塞。这个过程其实是通过在 server层 使用一种称之为 元数据锁(Metadata Locks,简称 MDL)结构来实现的
      • 一般情况下,不会使用 InnoDB存储引擎提供的表记别的 S锁 和 X锁,只会在一些特殊情况下,比方说 奔溃恢复过程中用到。比如,在系统变量 autocommit = 0,innodb_table_locks = 1 时,手动获取InnoDB存储引擎提供的表t的S锁或者X锁可以这么写:
        • LOCK TABLES t READ:InnoDB 存储引擎会对表t加表记别的S锁
        • LOCK TABLES t WRITE:InnoDB 存储引擎会对表t加表记别的X锁
      • 不过尽量避免使用InnoDB存储引擎的表上使用 LOCK TABLES 这样的手动锁表语句,它们并不会提供什么额外的保护,只是会降低并发能力而已。InnoDB的厉害之处还是实现了更细粒度的行锁。
      锁类型自己可选自己可写自己可操作其他表他人可读他人可写
      读锁否,等
      写锁否,等否,等
  2. 意向锁(intention lock)

    • InnoDB支持 多粒度锁:multiple granularity locking,它允许 行级锁 与 表级锁 共存,而意向锁就是其中的一种表锁

    • 意向锁分为两种

      • 意向共享锁(inention shared lock,IS):事务有意向对表中的某些行加共享锁(S锁)
      • 意向排他锁(intention exclusive lock,IX):事务有意向对表中的某些行加排他锁(X锁)
    • 意向锁是在当事务加表锁发挥作用。比如一个事务想要对表加排他锁,如果没有意向锁的话,那么该事务在加锁前需要判断当前表的每一行是否已经加了锁,如果表很大,遍历需要非常多的时间。有了意向锁,那么加锁表前,只需要判断当前表是否有意向锁即可,这样加快了对表锁的处理速度

    • 意向锁是存储引擎自己维护的,是内部机制,用户无法操作意向锁

    • 意向锁的并发性

      • 意向锁不会与行级的共享/排他锁互斥
    • 意向锁的作用

      1. InnoDB支持多粒度锁,特定场景下,行级锁可以与表级锁共存
      2. 意向锁之间互不排斥,但除了 IS与S兼容外,意向锁会与 共享锁 / 排他锁 互斥
      3. IX,IS是表级锁,不会和行级的X,S锁发生冲突。智慧和表级的X,S发生冲突
      4. 意向锁1在保证并发性的前提下,实现了行锁和表锁共存且满足事务隔离性的要求
  3. 自增锁(AUTO-INC锁)

    1. 插入的分类

      1. Simple inserts(简单插入)

        • 可以预先确定要输入的行数(当语句被初始处理时)的语句
      2. Bulk Inserts (批量插入)

        • 事先不知道要插入的行数(和所需自动递增值的数量)的语句。
      3. Minxed-mode inserts (混合模式插入)

        • 这些是 Simple inserts语句但是指定部分新行的自动递增值。有"INSERT ... ON DUPLICATE KEY UPDATE"
      • 对于上面数据插入的案例,MySQL采用了自增锁的方式来实现,AUTO-INC锁是当向使用含有 AUTO_INCREMENT列的表中插入数据时需要获取的一种特殊的表级锁,在执行插入语句时就在表级别加一个 AUTO-INC 锁,然后为每条待插入记录的 AUTO_INCREMENT 修饰的列分配递增的值,在该语句执行结束后,再把 AUTO-INC 锁释放掉。
      • 一个事务在持有 AUTO-INC 锁的过程中,其他事务的插入语句都要被阻塞,可以保证一个语句中分配的递增值是连续的。
      • 当向一个有 AUTO_INCREMENT关键字的主键插入值的时候,每条语句都要对这个表锁进行竞争,这样的并发潜力是很低下的,所以 innodb 通过 innodb_atuoinc_lock_mode 的不同取值来提供不同的锁定机制,以提高SQL语句的可伸缩性和性能
    2. innodb_atuoinc_lock_mode

      1. innodb_atuoinc_lock_mode = 0(传统 锁定模式)
        • 在此锁定模式下,所有类型的 insert 语句都会获得一个特殊的表级 AUTO-INC 锁,用于插入具有 AUTO_INCREMENT 列的表。
      2. innodb_atuoinc_lock_mode = 1(连续锁定模式)
        • 在这个模式下,bulk inserts 仍然使用 AUTO-INC 表级锁,并保持到语句结束。
        • 对于 Simple inserts,则通过在 mutes(轻量锁)的控制下获得所需数量的自动递增值来避免表级 AUTO-INC 锁,它只在分配过程的持续时间内保持,而不是直到语句完成。前提是其他事务没有拿到 AUTO-INC 锁
      3. innodb_atuoinc_lock_mode = 2 (交错锁定模式)
        • 在这种锁定模式下,所有类 INSERT 语句都不会使用表级 AUTO-INC 锁,并且可以同时执行多个语句。但是当使用基于语句的复制或恢复方案时,从二进制日志重播 SQL语句时,这是不安全的
        • 在此锁定模式下,自动递增值保证在所有并发执行的所有类型的 insert 语句中是 唯一 且单调递增的。但是,由于多个语句可以同时生成数字,为任何给定语句插入的行生成的值可能不是连续的。
    3. 元数据锁(MDL 锁)

      • MDL的作用是,保证读写的正确性。
      • 当对一个表增删改查的时候,加MDL读锁;当要对表做结构变更操作的时候,加 MDL 写锁
      • 读锁之间不互斥,因此可以有多个线程同时对一张表增删改查。读写锁之间、写锁之间是互斥的,写锁之间是互斥的,用来保证变更表结构操作的安全性,解决了 DML 和 DDL 操作之间的一致性问题。不需要显示使用,在访问一个表的时候会被自动加上
  4. 行锁

    1. 记录锁(Record Locks)
      • 记录锁就是把一条记录锁上,官方的类型名称为:LOCK_REC_NOT_GAP
        • 记录锁有 S锁和X锁之分
          • 当一个事务获取了一条记录的S型记录锁后,其他事务也可以继续获取该记录的S型记录锁,但不可以继续获取X型记录所
          • 当一个事务获取了一条记录的X型记录锁后,其他食物既不可以继续获取该记录的S型记录锁,也不可以继续获取X型记录锁
    2. 间隙锁(Gap Locks)
      • 间隙锁是为了解决幻读问题的。幻读问题的本质是插入表中不存在的行,事务无法给这些幻影记录上记录锁。InnoDB提出了一种称之为 Gap Locks 的锁。
      • GAP锁会锁住插入行和前一行之间的范围
      • GAP锁的提出是为了防止插入幻影记录而提出的。虽然有 共享 gap 锁和 独占gap 锁这样的说法,但是它们起到的作用是相同的。而且如果对一条记录加了 gap 锁(不论是共享 gap 锁还是独占 gap 锁),并不会限制其他事物对这条记录加记录锁或者继续加 gap 锁
    3. 临键锁(Next-key Locks)
      • 有时候我们既想锁住某条记录,又想组织其他事务在该记录前边的间隙插入新纪录,所以 innodb就提出了一种称之为 Next-Key Locks的锁,官方的类型名称为:LOCK_ORDINARY,也可以简称为 next-key 锁。Next-Key Locks是在存储引擎innodb、事务级别在可重复读的情况下使用的数据库锁
      • next-key锁的本质就是一个记录锁和一个gap锁的合体,它既能保护该条记录,又能组织别的事物将新记录插入被保护记录前边的间隙。
    4. 插入意向锁(Insert Intention Locks)
      • 一个事务在插入一条记录时需要判断一下插入位置是不是被别的事务加入了gap锁,如果有的话,插入需要等待,直到拥有 gap锁 的那个事务提交。但是InnoDB规定事务在等待的时候需要在内存中生成一个锁结构,表明有事务想在某个间隙中插入新纪录,但是现在在等待。InnoDB就把这种类型的锁命名为 Insert Intention Locks,官方的名称为:LOCK_INSERT_INTENTION,称为插入意向锁。插入意向锁是一种Gap锁,不是意向锁,在insert操作时产生
      • 插入意向锁并不会组织别的事务继续获取该记录上任何类型的锁
    5. 页锁
      • 页锁是在页的粒度上进行锁定,锁定的数据资源比行锁要多,因为一个页中可以多个行记录。当我们使用页所的时候,会出现数据浪费的现象,但这样的浪费最多也就是一个页上的数据行。页锁的开销介于表锁和行锁之间,会出现死锁。锁定粒度介于表锁和行锁之间,并发度一般
      • 每个层级的锁数量是有限制的,因为锁会占用内存空间,锁空间的大小是有限的。当某个层级的锁数量超过了这个层级的阈值时,会进行锁升级。

乐观锁、悲观锁

  • 乐观锁、悲观锁并不是锁,而是锁的设计思想
  1. 悲观锁(Pessimistic Locking)

    • 悲观锁是一种思想,对数据库被其他事务的修改持保守态度,会通过数据库自身的锁机制实现,从而保证数据操作的排他性
    • 悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次再拿数据的时候会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其他线程)
  2. 乐观锁(Optimistic Locking)

    • 乐观锁认为对同一数据的并发操作不会总发生,属于小概率时间,不用每次对数据上锁,但是在更新的时候会判断以下在此期间别人有没有去更新这个数据,也就是不采用数据库自身的锁机制,而是通过程序来实现。
    1. 乐观锁的版本号机制
      • 在表中设计一个 版本字段 version,第一次读的时候,会获取 version 字段的取值。然后对数据库进行更新或删除操作时,会执行 update ... set version = version + 1 where version = version。
    2. 乐观锁的时间戳机制
      1. 时间戳和版本号机制一样,也是在更新提交的时候,将当前数据的时间戳和更新之前取得的时间戳进行比较,如果两者一致则更新成功,否则就是版本冲突
  3. 两种锁的适用场景

    1. 乐观锁适合读操作多的场景,相对来说写的操作比较少。它的优点在于程序实现,不存在死锁问题。
    2. 悲观锁适合写操作多的场景,因为写的操作具有排他性。采用悲观锁的方式,可以在数据库层面阻止其他事务对该数据的操作权限,防止 读 - 写 和 写 - 写 的冲突

按加锁的方式划分:显示锁、隐式锁

  1. 隐式锁

    • 情景一:对于聚簇索引记录来说,有一个 trx_id 隐藏列,该隐藏列记录着最后改动该记录的事务 id。如果在当前事务中插入一条聚簇索引记录后,该记录的 trx_id 隐藏列代表的就是当前事务的事务id,如果其他事务此时想对该记录添加 S锁 或者 X锁 时,首先会看一下该记录的trx_id隐藏列代表的事务是否是当前的活跃事务,如果是的话,就帮助该记录的当前事务创建一个 X锁(is_waitings属性是 false),然后自己进入等待状态(为自己创建一个锁结构,is_waiting 属性是 true)
    • 情景二:对于二级索引记录来说,本身并没有trx_id 隐藏列,但是在二级索引页面的Page Header 部分有一个 PAGE_MAX_TRX_ID 属性,该属性代表对页面做改动的最大的事务id,如果 PAGE_MAX_TRX_ID 属性值小于当前最小的活跃事务id,那么说明对该页面做修改的事务都已经提交了,否则就需要找到回表找到对应的记录,重复情景一的做法
    • 隐式锁的逻辑过程
      1. InnoDB的每条记录中有一个隐含的 trx_id 字段,这个字段存在于聚簇索引的 B+Tree中
      2. 在操作一条记录前,首先根据记录中 trx_id 检查该事务是否是活动的事务 (未提交或回滚)。如果是活动的事物,首先将 隐式锁 转换为 显示锁
      3. 检查是否有锁冲突,如果有冲突,创建锁,并设置为 waiting 状态,如果没有冲突不加锁,跳到 5
      4. 等待加锁成功,被唤醒,或者超时
      5. 写数据,并将自己的trx_id 写入 trx_id 字段
  2. 显示锁

    • 通过特定的语句进行加锁,一般称之为显示加锁,例如:
    • 显示加共享锁:
    select ... lock in share mode
    
    • 显示加排他锁:
    select 。。。 for update
    

全局锁

  • 全局锁就是对整个数据库实例加锁,当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。全局锁的典型使用场景是:做全库逻辑备份
#全局锁的命令
Flush tables with read lock

死锁

  • 死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环。
  • 策略
    1. 一种策略是,直接进入等待,直到超时,这个超时时间可以通过参数 innodb_lock_wait_timeout 来设置
    2. 另一种策略是,发起死锁检测。发现死锁后,主动回滚死锁链条中的某一个事务(将持有最少行级排他锁的事务进行回滚),让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑
  • 成本分析
    • 如果能确保这个业务一定不会出现死锁,可以临时把死锁检测关掉
    • 控制并发度

锁的内存结构

  • 锁所在事务信息

    • 不论是表锁还是行锁,都是在事务执行过程中生成的,哪个事务生成了这个锁结构,这里就记录这个事务的信息。此锁所在的事务信息在内存结构中是一个指针,通过指针找到内存中关于该事务的更多信息,比方说事务id等
  • 索引信息

    • 对行锁来说,需要记录一下加锁的记录是属于哪个索引的。这里也是一个指针
  • 表锁/行锁信息

    • 表锁结构和行锁结构在这个位置的内容是不同的

      • 表锁

        • 记载着是对哪个表加的锁,还有其他的一些信息
      • 行锁

        • 记载了三个重要的信息

          • Space ID:记录所在表空间
          • Page Number:记录所在页号
          • n_bits:对于行锁来说,一条记录就对应着一个比特币,一个页面中包含很多记录,用不同的比特位来区分到底是哪一条记录加了锁。为此在行锁结构的末尾放置了一堆比特位,这个n_bits属性代表使用了多少比特位
            • n_bits的值一般比页面中记录数多一些。主要是为了之后在页面中插入了新纪录后也不至于重新分配锁结构
          • type_mode:
            • 这是一个32位的数,被分成了 lock_mode、lock_type 和 rec_lock_type 三个部分
            • 锁的模式(lock_mode),占用低四位
              • LOCK_IS(十进制的 0):表示共享意向锁,也就是 IS锁
              • LOCK_IX(十进制的 1):表示独占意向锁,也就是 IX锁
              • LOCK_S(十进制的2):表示共享锁,也就是 S锁
              • LOCK_X(十进制的3):表示独占锁,也就是 X锁
              • LOCK_AUTO_INC(十进制的4):表示 AUTO-INC锁
            • 锁的类型(lokc_type):占用 5~8位
              • LOCK_TABLE(十进制的16),也就是当第5个比特位置为1时,表示行级锁
              • LOCK_REC(十进制的32),也就是当第6个比特位置为1时,表示行级锁
            • 行锁的具体类型( rec_lock_type ),使用其余的位来表示。只有在lock_type 的值为LOCK_REC 时,也就是只有在该锁为行级锁时,才会被细分为更多的类型:
            • LOCK_ORDINARY (十进制的0 ):表示next-key锁。
              • LOCK_GAP (十进制的512 ):也就是当第10个比特位置为1时,表示gap锁。
              • LOCK_REC_NOT_GAP (十进制的1024 ):也就是当第11个比特位置为1时,表示正经记录锁。
              • LOCK_INSERT_INTENTION (十进制的2048 ):也就是当第12个比特位置为1时,表示插入意向锁。其他的类型:还有一些不常用的类型我们就不多说了。
              • is_waiting 属性呢?
                • 基于内存空间的节省,所以把is_waiting 属性放到了type_mode 这个32位的数字中
                • LOCK_WAIT (十进制的256 ) :当第9个比特位置为1 时,表示is_waiting 为true ,也 就是当前事务尚未获取到锁,处在等待状态;当这个比特位为0 时,表示is_waiting 为 false ,也就是当前事务获取锁成功。
  • 其他信息

    • 为了更好的管理系统运行过程中生成的各种锁结构而设计了各种哈希表和链表。
  • 一堆比特位

    • 如果是行锁结构的话,在该结构末尾还放置了一堆比特位,比特位的数量是由上边提到的n_bits 属性表示的。InnoDB数据页中的每条记录在记录头信息中都包含一个heap_no 属性,伪记录Infimum的heap_no 值为0 , Supremum 的heap_no 值为1 ,之后每插入一条记录, heap_no 值就增1。锁结构最后的一堆比特位就对应着一个页面中的记录,一个比特位映射一个heap_no ,即一个比特位映射到页内的一条记录。

锁监控

  • 关于MySQL锁的监控,一般可以通过检查 InnoDB_row_lock 等状态变量来分析系统上的行锁的争夺情况
show status like 'innodb_row_lock%'
  • 对各个状态量的说明
    • Innodb_row_lock_current_waits:当前正在等待锁定的数量;
    • Innodb_row_lock_time :从系统启动到现在锁定总时间长度;(等待总时长)
    • Innodb_row_lock_time_avg :每次等待所花平均时间;(等待平均时长)
    • Innodb_row_lock_time_max:从系统启动到现在等待最常的一次所花的时间;
    • Innodb_row_lock_waits :系统启动后到现在总共等待的次数;(等待总次数)

其他监控方法

  • MySQL把事务和锁的信息记录在了information_schema 库中,涉及到的三张表分别是INNODB_TRX 、INNODB_LOCKS 和INNODB_LOCK_WAITS 。
  • MySQL5.7及之前,可以通过information_schema.INNODB_LOCKS查看事务的锁情况,但只能看到阻塞事务的锁;如果事务并未被阻塞,则在该表中看不到该事务的锁情况。
  • MySQL8.0删除了information_schema.INNODB_LOCKS,添加了performance_schema.data_locks ,可以通过performance_schema.data_locks查看事务的锁情况,
  • 和MySQL5.7及之前不同,performance_schema.data_locks不但可以看到阻塞该事务的锁,还可以看到该事务所持有的锁。同时,information_schema.INNODB_LOCK_WAITS也被performance_schema.data_lock_waits 所代替

MVCC篇

概念

  • MVCC(Multiversion Concurrency Control),多版本并发控制。MVCC是通过数据行的多个版本管理来实现数据库的并发控制。

快照读和当前读

  • MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理 读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读,而这个读指的就是快照读,而非当前读。

快照读

  • 快照读又叫一致性读,读取的是快照数据。不加锁的简单的SELECT 都属于快照读,即不加锁的非阻塞读

当前读

  • 当前读读取的是记录的最新版本(最新数据,而不是历史版本的数据),读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。

和MVCC相关的二个隐藏字段

  • trx_id:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务id赋值给trx_id 隐藏列
  • roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到 undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息

MVCC实现原理之 ReadView

  • MVCC的实现依赖于:隐藏字段、Undo Log、Read View

概念

  • 在MVCC机制中,多个事务对同一个行记录进行更新会产生多个历史快照,这些历史快照保存在 Undo Log里。
  • ReadView 就是事务在使用MVCC机制进行快照读操作时产生的读视图。当事务启动时,会生成数据库系统当前的一个快照,InnoDB还为每个事务构造了一个数组,用来记录并维护系统当前活跃事务的ID

设计思路

  • 使用READ UNCOMMITTED 隔离级别的事务,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本
  • 使用 SERIALIZABLE 隔离级别的事务,InnoDB规定使用加锁的方式来访问记录
  • 使用 READ COMMITTED 和 REPEATABLE READ 隔离级别的事务,必须保证读到 已经提交了的 事务修改过的记录。
    • 核心问题是需要判断一下版本链中的哪个版本是当前事务可见的
  • ReadView中主要包含的4个比较重要的内容
    1. creator_trx_id,创建这个 Read View的事务ID(只有对表做改动时,才会为事务分配事务id,否则在一个只读事务中的事务id值默认为0)
    2. trx_ids:表示在生成ReadView时当前系统中活跃的读写事务的 事务id列表
    3. up_limit_id:活跃的事务中最小的事务 ID
    4. low_limit_id:表示生成 ReadView 时系统中应该分配给下一个事务的 id 值。low_limit_id是系统最大的事务id值,这里要注意系统中的事务id,需要区别正在活跃的事务id

ReadView规则

  1. 如果被访问版本的 trx_id 属性值与ReadView中的 creator_trx_id 值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问
  2. 如果被访问版本的 trx_id 属性值小于 ReadView 中的 up_limit_id 值,表明生成该版本的事务在当前事务生成 ReadVIew 前已经提交,所以该版本可以被当前事务访问
  3. 如果被访问版本的 trx_id 属性值大于或等于 ReadView 中的 low_limit_id 值,表明生成该版本的事务在当前事务生成 ReadView 后才开启,所以该版本不可以被当前事务访问。
  4. 如果被访问版本的 trx_id 属性值在 ReadView 的 up_limit_id 和 low_limit_id之间,那就需要判断一下 trx_id 属性值是不是在 trx_ids 列表中
    • 如果在,说明创建 ReadView时生成该版本的事务还是活跃的,该版本不可以被访问
    • 如果不在,说明创建 ReadView时生成该版本的事务已经被提交,该版本可以被访问

总结

  • MVCC在 READ COMMITTED、REPEATABLE READ 这两种隔离级别的事务在执行快照读操作时访问记录的版本过程
  • 核心点在于 ReadView 的原理,READ COMMITTD、REPEATABLE READ这两个隔离级别的一个很大不同就是生成 ReadView 的时机不同:
    • READ COMMITTED的每一次进行普通 SELECT 操作前都会生成一个 ReadView
    • REPEATABLE READ 只在第一次进行普通 SELECT 操作前生成一个 ReadView,之后的查询操作都重复使用这个 ReadView 就好了。