MySQL -- 14 -- MySQL(InnoDB)当前读和快照读

官方文档:Gap Locks

官方文档:Next-Key Locks


一、当前读

  • 当前读,会对读取的记录加锁,保证其他并发事务不会修改当前记录,读取的是记录的最新版本

  • 简单来说,当前读就是加了锁的增删改查语句,不管上的共享锁还是排他锁,均为当前读

  • 相关 SQL:select ... lock in share modeselect ... for updateupdatedeleteinsert

  • 实现方式

    • 行锁

      • 是对单行记录上的锁

      • 行锁 + Gap 锁的组合称为 next-key

    • Gap 锁 (间隙锁)

      • Gap 锁是指对索引记录之间的间隙加的锁,即锁定一个范围,保证该范围内的数据在锁定情况下不会发生改变

      • MySQL 在读未提交 (Read Uncommitted)、读提交 (Read Committed) 隔离级别下不存在 Gap 锁;在可重复读 (Repeatable Read)、串行化 (Serializable) 隔离级别下存在 Gap 锁

    • 对主键索引或者唯一索引如何使用 Gap 锁?

      • 如果 where 条件全部命中,则不会使用 Gap 锁,只会使用行锁

      • 如果 where 条件部分命中或全不命中,则会使用 Gap 锁

    • 对非唯一索引或不走索引如何使用 Gap 锁?

      • 非唯一索引

        • 会对要修改记录的周边 Gap 进行加锁
      • 不走索引

        • 会对所有 Gap 进行加锁,相比表锁,此种情况代价更大,会降低 MySQL 的效率,通常需要避免
    • MySQL 在可重复读 (Repeatable Read) 隔离级别下如何避免幻读?

      • 通过引入 next-key 锁来避免
  • 举例说明

    • name 为主键索引,id 为唯一索引

      +------+----+
      | name | id |
      +------+----+
      | f    |  1 |
      | h    |  2 |
      | b    |  3 |
      | a    |  5 |
      | c    |  6 |
      | d    |  9 |
      +------+----+
      
      • where 条件全部命中

        • Session A 开启事务 start transaction;,并调用 delete from tbl_test1 where id = 9; 删除 id 为 9 的记录 (已存在)

        • Session B 开启事务 start transaction;,并调用 insert into tbl_test1 values('g', 10); 插入 id 为 10 的记录

        • 此时 Session A 尚未提交,由于 where 条件全部命中,所以会对 id 为 9 的记录加上行锁,因此不会阻塞 Session B 插入 id 为 10 的记录

        • Session A 与 Session B 提交事务 commit;

      • where 条件部分命中

        • Session A 开启事务 start transaction;,并调用 select * from tbl_test1 where id in (5,7,9) lock in share mode; 查询 id 为 5,7,9 的记录 (5,9 存在,7 不存在)

        • Session B 开启事务 start transaction;,并调用 insert into tbl_test1 values('g', 8); 插入 id 为 8 的记录

        • 此时 Session A 尚未提交,由于 where 条件部分命中,所以会对 id 为 5 到 9 之间的间隙加上 Gap 锁 (范围为:(5, 9)),因此会阻塞 Session B 插入 id 为 8 的记录

        • Session A 与 Session B 提交事务 commit;

        • PS:此处不使用 delete from tbl_test1 where id in (5,7,9); 进行举例的原因是该 delete 语句不会走索引,对所有间隙加上 Gap 锁

      • where 条件全不命中

        • Session A 开启事务 start transaction;,并调用 delete from tbl_test1 where id = 7; 删除 id 为 7 的记录 (不存在)

        • Session B 开启事务 start transaction;,并调用 insert into tbl_test1 values('g', 8); 插入 id 为 8 的记录

        • 此时 Session A 尚未提交,由于 where 条件全不命中,所以会对 id 为 7 的记录周围的间隙加上 Gap 锁 (范围为:(6, 9)),因此会阻塞 Session B 插入 id 为 8 的记录

        • Session A 与 Session B 提交事务 commit;

    • name 为主键索引,id 为普通索引

      +------+----+
      | name | id |
      +------+----+
      | h    |  2 |
      | c    |  6 |
      | b    |  9 |
      | d    |  9 |
      | f    | 11 |
      | a    | 15 |
      +------+----+
      
      • 插入记录的 id 处于中间状态

        • Session A 开启事务 start transaction;,并调用 delete from tbl_test2 where id = 9; 删除 id 为 9 的记录

        • Session B 开启事务 start transaction;,并调用 insert into tbl_test2 values('g', 9); 插入 id 为 9 的记录

        • 此时 Session A 尚未提交,会对 id 为 6 到 11 之间的间隙加上 Gap 锁 (范围为:(6, 11]),因此会阻塞 Session B 插入 id 为 9 的记录

        • Session A 与 Session B 提交事务 commit;

      • 插入记录的 id 处于临界状态

        • 在实践过程中,我们会发现对于临界状态,还需要加上主键的值,才能做出最精准的判断

        • Session A 开启事务 start transaction;,并调用 delete from tbl_test2 where id = 9; 删除 id 为 9 的记录

          • 插入 ('bb', 6) 记录

            • Session B 开启事务 start transaction;,并调用 insert into tbl_test2 values ('bb', 6); 插入 id 为 6,name 为 bb 的记录

            • 此时 Session A 尚未提交,会对 id 为 6 到 11 之间的间隙加上 Gap 锁 (范围为:(6, 11]),且根据主键 name 字段的排列,('bb', 6) 该行数据位于 (6, 11] 区间之外,因此不会阻塞 Session B 插入 id 为 6,name 为 bb 的记录

            • Session A 与 Session B 提交事务 commit;

          • 插入 ('dd', 6) 记录

            • Session B 开启事务 start transaction;,并调用 insert into tbl_test2 values ('dd', 6); 插入 id 为 6,name 为 dd 的记录

            • 此时 Session A 尚未提交,会对 id 为 6 到 11 之间的间隙加上 Gap 锁 (范围为:(6, 11]),根据主键 name 字段的排列,('dd', 6) 该行数据位于 (6, 11] 区间之内,因此会阻塞 Session B 插入 id 为 6,name 为 dd 的记录

            • Session A 与 Session B 提交事务 commit;

    • name 为主键索引,id 没有索引

      +------+----+
      | name | id |
      +------+----+
      | h    |  2 |
      | c    |  6 |
      | b    |  9 |
      | d    |  9 |
      | f    | 11 |
      | a    | 15 |
      +------+----+
      
      • Session A 开启事务 start transaction;,并调用 delete from tbl_test2 where id = 9; 删除 id 为 9 的记录

      • Session B 开启事务 start transaction;,并调用 insert into tbl_test2 values('g', 2); 插入 id 为 2 的记录

      • 此时 Session A 尚未提交,由于 id 没有索引,所以会对所有间隙加上 Gap 锁,因此会阻塞 Session B 插入 id 为 2 的记录

      • Session A 与 Session B 提交事务 commit;


二、快照读

  • 快照读,不会对读取的记录加锁,读取的可能是记录的最新版本,也可能是记录的历史版本

  • 简单来说,快照读就是不加锁的非阻塞读,即简单的 select 操作 (不包括 select ... lock in share modeselect ... for update)

  • 实现方式

    • 数据行里的 DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID 字段

      字段 含义
      DB_TRX_ID 最后一次修改本行记录的事务 ID
      DB_ROLL_PTR 回滚指针
      指向本行记录的上一个版本 (存储在 rollback segment 的 undo log中)
      DB_ROW_ID 自增行 ID
      如果所在表没有主键,InnoDB 会自动以 DB_ROW_ID 产生一个聚集索引
    • undo log

      • insert undo log

        • 是指事务对 insert 操作中产生的 undo log,只在事务回滚时需要,在事务提交后就可以立即删除
      • update undo log

        • 是指事务对 delete、update 操作中产生的 undo log,不仅在事务回滚时需要,在快照读中也需要 (即该 undo log 需要提供 MVCC 机制),所以不能随便删除,只有当数据库所使用的快照中不涉及该日志记录,对应的 undo log 才会被清理 (purge) 线程删除

        • 流程如下

          • 先用排他锁锁定当前行,然后将旧纪录拷贝一份到 rollback segment 中形成 undo log

          • 接着修改当前行的值,设置 DB_TRX_ID 为当前事务 ID,使用 DB_ROLL_PTR 指向旧纪录形成的 undo log,这样就能通过 DB_ROLL_PTR 找到该记录的历史版本

          • 如果对同一行记录进行连续的 update 操作,就会形成一条该记录 undo log 的链表,遍历这条链表可以看到该行记录的历史变迁

    • MVCC

      • MVCC (Multi-Version Concurrency Control),即多版本并发控制

      • MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存

      • MVCC 在 MySQL (InnoDB) 中的实现主要是为了提高数据库的并发性能,即读不加锁,读写不冲突,在读多写少的系统中,读写不冲突是非常重要的,极大地提高了系统的并发性能

    • read view

      • read view 主要用于可见性的判断,即当我们执行快照读时,会针对我们查询的记录去创建一个 read view (用于维护系统其他活跃事务 ID),来决定当前事务能看到的是哪一个版本的记录,有可能是最新版本的记录,也有可能是历史版本的记录

      • read view 遵循一个可见性算法,即将要修改记录的 DB_TRX_ID 取出来 (即当前事务 ID),与系统其他活跃事务 ID 进行做对比,这些活跃事务 ID 的最小值为 up_limit_id,最大值为 low_limit_id

        • DB_TRX_ID < up_limit_id

          • 当前记录的最后一次修改在创建 read view 之前,对当前事务可见
        • DB_TRX_ID > low_limit_id

          • 当前记录的最后一次修改在创建 read view 之后,对当前事务不可见,需要通过 DB_ROLL_PTR 从 undo log 中取出当前记录上一层的 DB_TRX_ID,直到取出的 DB_TRX_ID 小于系统活跃事务 ID 为止
        • up_limit_id <= DB_TRX_ID <= low_limit_id

          • 需要判断 DB_TRX_ID 是否包含在活跃事务 ID 中

            • 如果包含,则说明当前记录的最后一次修改尚未提交,对当前事务不可见,通过 DB_ROLL_PTR 从 undo log 中取出当前记录上一层的 DB_TRX_ID,直到取出的 DB_TRX_ID 小于系统活跃事务 ID 为止

            • 如果不包含,则说明当前记录的最后一次修改已经提交,对当前事务可见

      • 在 Repeatable Read 隔离级别下,session 在开启事务后的第一条快照读会创建一个快照 (read view),将当前系统中活跃的其他事务记录下来,之后再调用快照读的时候,使用的还是同一个快照 (read view),也就是说,如果首次使用快照读是在别的事务对记录做出增删改并提交之前的,那么此后,即便别的事务对记录做了增删改并提交,读取到的仍然是修改之前的数据

      • 在 Read Committed 隔离级别下,session 在开启事务后的每一条快照读都会创建一个新的快照 (read view),这就是为什么在 RC 下,能用快照读看到别的事务提交的对表记录的增删改;

  • 举例说明

    • 有个 ID 为 10 的事务往表 tbl_student 中插入如下数据

      id name age
      1 Tom 22
    • 有个 ID 为 20 的事务要对该条数据进行修改,将 Tom 改为 Tom1,但并未提交

      update tbl_student set name = Tom1 where id = 1;
      
    • 此时,会在 undo log 中形成一条版本链,如下所示

      MySQL -- 14 -- MySQL(InnoDB)当前读和快照读_第1张图片

    • 在形成版本链后,有个 ID 为 30 的事务调用了 select * from tbl_student where id = 1 来查询 id 为 1 的记录,此时会生成一个 read view 来维护活跃事务 ID,此处 ID 为 20、30 的事务处于活跃状态,即活跃事务 ID 列表为 [20, 30]

    • ID 为 30 的事务会顺着版本链开始查找,先查找到 name 为 Tom1 的记录,发现 DB_TRX_ID 为 20,在活跃事务 ID 列表内,所以无法访问,顺着指针继续查找下一条, name 为 Tom 的记录,发现 DB_TRX_ID 为 10,小于活跃事务 ID 列表内的最小值,所以可以访问,即此时 ID 为 30 的事务访问到的是 name 为 Tom 的记录

    • 提交 ID 为 20 的事务,此时又有一个 ID 为 40 的事务要对该条数据进行修改,将 Tom1 改为 Tom2,但并未提交

      update tbl_student set name = Tom2 where id = 1;
      
    • 此时,会在 undo log 中形成一条版本链,如下所示

      MySQL -- 14 -- MySQL(InnoDB)当前读和快照读_第2张图片

    • 在形成版本链后,有个 ID 为 50 的事务调用了 select * from tbl_student where id = 1 来查询 id 为 1 的记录,注意此处在不同的隔离级别下会有不同的表现

      • 在读未提交 (Read Uncommitted) 隔离级别下,会重新生成一个 read view,此时活跃事务 ID 列表为 [40, 50],按照上面的逻辑,通过版本链 ID 为 50 的事务访问到的是 name 为 Tom1 的记录

      • 在读提交 (Read Committed) 隔离级别下,不会重新生成一个 read view,使用的依旧是第一次快照读生成的 read view,此时活跃事务 ID 列表为 [20, 30],按照上面的逻辑,通过版本链 ID 为 50 的事务访问到的是 name 为 Tom 的记录


三、参考资料

  • MVCC多版本并发控制

  • innodb当前读 与 快照读

  • MySQL InnoDB MVCC深度分析

  • MySQL当前读、快照读、MVCC

  • 面试官:谈谈你对Mysql的MVCC的理解?

你可能感兴趣的:(MySQL,MySQL,Next-Key,Locks,Gap,Locks)