写点什么

InnoDB 存储引擎 - 锁

用户头像
CodeWithBuff
关注
发布于: 13 小时前

什么是锁

锁存在的意义是为了支持对共享资源的并发访问,以及保证数据的一致性和完整性。

lock 和 latch

latch 一般称为闩锁,是一种轻量级的锁,它要求锁定时间必须非常短。在 InnoDB 中,latch 的实现有两种,分别是互斥锁和读写锁。


lock 的对象是事务,用来锁定数据库中的对象,比如表,页,行。lock 的对象仅在事务 commit 或者 rollback 之后释放


latch 没有死锁检测机制,但是 lock 有。

InnoDB 存储引擎中的锁

锁的类型

InnoDB 实现了两种标准的行级锁


  • 共享锁(S Lock),允许多个事务同时读,事务写会阻塞。

  • 排他锁(X Lock),仅允许一个事务写,其余事务的读和写都会阻塞。



为了实现更好的加锁,InnoDB 支持意向锁,什么意思呢?就是划分出更细粒度的加锁对象,组成对象组织树形式。每次给细粒度加锁,就会给粗粒度加一个同类型的意向锁。



意向锁也有两种:IX 和 IS


比如想要对行记录加 X 锁,就会给表加一个 IX 锁。意向锁用来实现对粗粒度加 X/S 锁时是否有细粒度冲突的快速判断


如果表已经有了 IX 锁,则说明在这个表里,至少有一行在使用 X 锁,所以想对这个表加 X 或 S 就是不可以的。此时就可以通过表的意向锁快速判断,而不需要遍历行锁。


所以如果某个行加 X,另一行加 S,即使此时会在同一个表添加 IX 和 IS 两个意向锁,但是意向锁之间不会冲突,意向锁仅仅说明,在当前粒度下,还有属于它的更小粒度加了相应的 X/S 锁



图示的 X/S 均是表锁,IS/IX 均是对行加锁产生的表级意向锁,这张表阐述的是表级锁和表级意向锁之间的兼容性

一致性非锁定读

一致性非锁定读是通过 MVCC 来实现对某一行数据的读取不会因为这行数据的 X 锁而被阻塞。通过定义可以看出,这种读取方式读取的是此行的历史数据,历史数据的保存是通过快照保存的。



快照通过 undo 段实现,undo 段用来回滚事务。同时历史数据不会被更改,所以访问快照不需要锁操作。此外,因为一个行记录可能有多个快照,所以称为 MVCC(多版本并发控制)。


InnoDB 对于 ReadCommited 以及 RepeatableRead 默认使用这种读取方式。但是这两个隔离界别的非锁定读的实现略有差别,前者要求每次读取最新的快照,后者要求读取事务开始时的行数据版本。

一致性锁定读

虽然一致性非锁定读通过快照实现了更好的并发,但是有时候我们需要保证数据逻辑的强一致性,此时就需要使用加锁版本的一致性锁定读。


为了在读取时加上锁,,我们需要使用加锁的 Select 语句。InnoDB 支持两种加锁 Select:


select ... for updateselect ... in share mode
复制代码


第一个会对读取的行加上 X 锁,第二个会加上 S 锁。在使用这两个语句时,需要手动开启事务提交。

自增长与锁

为了实现自增主键的+1 操作,需要使用 AUTO-INC Locking 锁机制实现,这种锁是表锁,它会在完成自增长插入的 SQL 语句结束后自动释放,以此来提升性能,而不是事务结束。


后面引入了轻量级互斥量的自增长实现机制,提升了性能。

外键和锁

对于外键值的插入或更新,首先需要查询父表中的记录,也就是 Select 全表,此时会对父表加一个 S 锁。

锁的算法

行锁的三种算法

InnoDB 有三种行锁的算法:


  • Record Lock:锁住单条记录。

  • Gap Lock:锁住一个范围,但是不包括当前记录。

  • Next-Key Lock:锁住一个范围同时包含当前记录,相当于 Record Lock+Next-Key Lock。


RecordLock 锁住的是索引,如果当前 where 后面的列没有索引,那么就会锁住主键。Next-Key Lock 锁住一个范围,这样是为了解决可重复读的幻读问题,即前后读到的数据量不一致。


当 Select 一个范围时,如果 where 后面的列拥有唯一索引,那么就会使用 Record Lock 替代 Next-Key Lock,以此来提高系统并发性。对于主键索引亦是如此;但是对于辅助索引,则会使用范围锁定,来避免有其他操作在这个范围内增删。


此外,InnoDB 还会为辅助索引的下一个键值加上 GapLock,其目的是为了阻止幻读问题。


在进行范围锁定时,也会把锁定的区间所包含的主键区间进行一同锁定。


现在来理一理这三个锁锁的范围:



InnoDB 引入范围锁是为了解决幻读,在这里再多嘴几句。


  • 为什么行锁解决不了幻读?因为行锁无法在不存在的行上加锁,插入是创造原本不存在的行。

  • 为什么还要在键后面区间加锁?因为插入操作在插入键相同时,插入在当前行后面,所以后面还要加个区间锁,前面区间理所当然需要加锁。


所以对于一个 Select ... For Update 操作会锁定:(prev_id, curr_id] + (curr_id, next_id)。


参考


1


2


3


对于插入操作,会判断插入键下一个值是否被锁定,如果被锁定则阻塞。

解决幻读

幻读主要是指在同一个事务的多次 Select 时,后面读取到了前面读取的不存在的行,也就是事务执行期间,有别的事务在 Select 键区间插入了新的值。


InnoDB 默认隔离级别 Repeatable Read 支持 Next-Key Lock,这样可以避免幻读。

锁问题

脏读

脏数据和脏页是不一样的概念,脏页是未刷新到磁盘的数据,脏数据是事务未提交的 SQL 操作。


脏读就是某个事务读取到了别的事务未提交的操作。


脏读违反了隔离性。

不可重复读

在某个事务读取某个数据集合的时间内,另一个事务对这个数据集合做出了 DML 操作,致使前一个事务多次读取中读取到的数据发生了变化(数据总量没变)。


不可重复读违反了一致性,因为很明显,在别的事务读数据时,有事务进行了数据更新。

丢失更新

简单来说就是事务 A 作出的更新还未提交时,事务 B 进行了另一个更新,覆盖了事务 A 的更新,导致事务 A 的更新丢失。


解决方式可以是使用串行隔离级别。

死锁

解决死锁的简单方法就是设置超时,然后回滚。但是这有一个问题,如果某个事务占用了较多的 undo,或者权重大,那么超时回滚就显得不那么合理了。


所以现在的引擎大多采用等待图来判断死锁,这点和 OS 的实现如出一辙。简单来说就是记录每个事务需要的资源,然后使用指针,A->B 表示事务 A 需要事务 B 的锁。一旦出现回路就表示存在死锁。


这是一种较为主动的死锁检测机制,每次发生死锁,回滚 undo 较小的事务。改进的算法由 DFS->循环实现回路判断。

锁升级

如果对行加 1 万个锁,那还不如直接对表加锁来的快,这就是锁升级,但是 InnoDB 暂不支持,所以略去不表。

发布于: 13 小时前阅读数: 5
用户头像

CodeWithBuff

关注

还未添加个人签名 2021.03.13 加入

还未添加个人简介

评论

发布
暂无评论
InnoDB存储引擎-锁