MySql进阶-间隙锁(gap-key)

  • Post author:
  • Post category:mysql



可参考



快照读,当前读可参考


参考《InnoDB存储引擎》

注意:gap-key是innodb存储引擎来解决当前读的幻读问题的。对于隔离级别下的可重复读只能解决快照读的幻读问题。

  • 快照读, 读取专门的快照 (对于RC,快照(ReadView)会在每个语句中创建。对于RR,快照是在事务启动时创建的)

    简单的select操作即可

    针对的也是select操作
  • 当前读, 读取最新版本的记录, 没有快照。 在InnoDB中,当前读取根本不会创建任何快照。

    select … lock in share mode

    select … for update

    insert

    update

    delete

    针对如下操作, 会让如下操作阻塞:

    insert

    update

    delete


在RR(可重复读)级别下, 快照读是通过MVVC(多版本控制)和undo log来实现的,

当前读是通过手动加record lock(记录锁)和gap lock(间隙锁)来实现的。所以从上面的显示来看,如果需要实时显示数据,还是需要通过加锁来实现。这个时候会使用next-key技术来实现。

tips:SELECT…FOR UPDATE对读取的行记录加一个X锁,其他事务不能对已锁定的行加上任何锁。SELECT…LOCK IN SHARE MODE对读取的行记录加一个S锁,其他事务可以向被锁定的行加S锁,但是如果加X锁,则会被阻塞。

使用这两种一致性锁定读的办法注意开启事务,提交事务,来锁定与释放锁。



Innodb锁算法

  • Record Lock:单个行记录上的锁
  • Gap Lock:间隙锁,锁定一个范围,但不包含记录本身
  • Next-Key Lock∶Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身

对于Record Lock会去锁索引,如果表在建立的时候没有设置任何一个索引,那么innodb会使用隐式的主键来当这个索引来锁定。

Gap Lock的作用是为了阻止多个事务将记录插入到同一范围内。


InnoDB存储引擎会对Next-Key Lock进行优化,将其降级为Record Lock,即仅锁住索引本身,而不是范围。这种情况发生在查询的列是唯一索引的情况下。

若唯一索引由多个列组成,而查询仅是查找多个唯一索引列中的其中一个,那么如果查询其实是范围类型查询,而不是精准类型查询,InnoDB存储引擎会使用Next-Key Lock进行锁定。

在InnoDB存储引擎中,对于INSERT的操作,其会检查插入记录的下一条记录是否被锁定,若已经被锁定,则不允许查询。



关闭Gap Lock

  • 将事务的隔离级别设置为READ COMMITTED
  • 将参数innodb_locks_unsafe_for_binlog设置为1



Gap-key 解决的问题

在默认的事务隔离级别下,即REPEATABLE READ下,InnoDB存储引擎采用Next-Key Locking机制来避免

幻读

。幻读会导致即使把所有的记录都加上锁,还阻止不了新插入的记录。

在同一事务下,连续执行两次同样的SQL语句可能导致不同的结果,第二次的SQL语句可能会返回之前不存在的行。

在可重复读下,两次当前读语句可能会出现幻读问题。而Gap-Key便是用来解决当前读的幻读的问题的。

举例(细节略),该例子是说明在可重复读下,mysql在没有解决幻读时,两个事务读会出现幻读的问题。:

事务1 事务2
select * from table where int>2 for update
结果 3 5
insert into table (4)
select * from table where int >2 for update
结果3 4 5

由例子可以看出,如果不加间隙缩,事务的两次当前读会读到之前没有行,即幻读。

间隙锁和行锁合称 next-key lock,每个 next-key lock 是前开后闭区间。在同一事务内,前后两次执行同一sql语句,结果不一样。而当使用Gap-key的时候,就可以在 (2,+∞)加上锁,禁止其他事务进行插入。

InnoDB存储引擎默认的事务隔离级别是REPEATABLE READ,在该隔离级别下,Innobb其采用Next-Key Locking的方式来加锁,解决幻读问题。

对于非索引字段进行update,delete或select … for update操作,代价极高。所有记录上锁,以及所有间隔的锁。

对于索引字段进行上述操作,代价一般。只有索引字段本身和附近的间隔会被加锁。

对于select for update 如果不走索引,那么会走全表扫描,增加的gap_lock会非常多。

主键索引的间隙上也要有Gap lock保护的。

MVCC 具体解决了以下问题:

并发读-写时:可以做到读操作不阻塞写操作,同时写操作也不会阻塞读操作。

解决脏读、幻读、不可重复读等事务隔离问题,但不能解决上面的写-写(需要加锁)问题。



间隙锁影响

间隙锁的引入,可能会导致同样的语句锁住更大的范围,这其实是影响了并发度的。

如果把隔离级别设置为读提交的话,就没有间隙锁了。但同时,你要解决可能出现的数据和日志不一致问题,需要把 binlog 格式设置为 row。



MVCC 核心原理


可参考

它的实现原理主要是版本链,undo日志 ,Read View来实现的。

InnoDB 存储引擎的介绍了数据页的行格式,对于使用它的表来说,表中的聚簇索引都包含三个隐藏列:

在这里插入图片描述

每次对记录进行改动,都会记录一条 undo 日志,每条 undo 日志也都有一个 roll_pointer 属性(INSERT 操作对应的 undo 日志没有该属性,因为该记录并没有更早的版本),可以将这些 undo 日志都连起来,串成一个链表,所以现在的情况就像下图一样:

在这里插入图片描述

对该记录每次更新后,都会将旧值放到一条 undo 日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值。

另外,每个版本中还包含生成该版本时对应的事务 id。于是可以利用这个记录的版本链来控制并发事务访问相同记录的行为,那么这种机制就被称之为多版本并发控制(MVCC)。



ReadView

改动的记录都存在在 undo 日志中,那如果一个日志需要查询行记录,需要读取哪个版本的行记录呢?

1️⃣ 对于使用 READ UNCOMMITTED 隔离级别的事务来说,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了。

2️⃣ 对于使用 SERIALIZABLE 隔离级别的事务来说,InnoDB 使用加锁的方式来访问记录,不存在并发问题。

3️⃣ 而对于使用 READ COMMITTED 和 REPEATABLE READ 隔离级别的事务来说,都必须保证读到已经提交了的事务修改过的记录,也就是说假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的。

核心问题就是: READ COMMITTED 和 REPEATABLE READ 隔离级别在不可重复读和幻读上的区别在哪里?这两种隔离级别对应的不可重复读与幻读都是指同一个事务在两次读取记录时出现不一致的情况,这两种隔离级别关键是需要判断版本链中的哪个版本是当前事务可见的。

ReadView 就是用来解决这个问题的,可以帮助我们解决可见性问题。 事务进行快照读操作的时候就会产生 Read View,它保存了当前事务开启时所有活跃的事务列表。

注:这里的活跃指的是未提交的事务。

每一个事务在启动时,都会生成一个 ReadView,用来记录一些内容,ReadView 中主要包含 4 个比较重要的属性:

在这里插入图片描述

其中,max_trx_id并不是指m_ids中的最大值,因为事务 id 是递增分配的,假如现在有 id 为 1,2,3 这三个事务,之后 id 为 3 的事务提交了。那么一个新的读事务在生成 ReadView 时,m_ids 就包括 1 和 2,min_trx_id 的值就是 1,max_trx_id 的值就是 4。

再有了 ReadView 之后,在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见:

  • trx_id = creator_trx_id ,可访问

    如果被访问版本的 trx_id 属性值与 ReadView 中的 creator_trx_id 值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。

  • trx_id < min_trx_id ,可访问

    如果被访问版本的 trx_id 属性值小于 ReadView 中的 min_trx_id 值,表明生成该版本的事务在当前事务生成 ReadView 前已经提交,所以该版本可以被当前事务访问。

  • trx_id >= max_trx_id ,不可访问

    如果被访问版本的 trx_id 属性值大于或等于 ReadView 中的 max_trx_id 值,表明生成该版本的事务在当前事务生成 ReadView 后才开启,所以该版本不可以被当前事务访问。

  • min_trx_id <= trx_id < max_trx_id,存在 m_ids 列表中不可访问

    如果被访问版本的 trx_id 属性值在 ReadView 的 min_trx_id 和 max_trx_id 之间,那就需要判断一下 trx_id 属性值是不是在 m_ids 列表中,如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。

  • 某个版本的数据对当前事务不可见

  • 如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录。

在 MySQL 中,READ COMMITTED 和 REPEATABLE READ 隔离级别的的一个非常大的区别就是它们生成 ReadView 的时机不同。



版权声明:本文为littlewhitevg原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。