InnoDB 锁机制解析

来自Wikioe
跳到导航 跳到搜索


关于

看了 MySQL 官方手册,但是对于 InnoDB 锁理解还是不够,所以,又在网上找了一些文章,摘录再次。

有时间还是看看相关书籍,如:《高性能MySQL》、《MySQL技术内幕:InnoDB存储引擎》
转自【MySQL InnoDB锁机制全面解析分享

为什么要加锁?

锁机制用于管理对共享资源的并发访问。防止不同事务对相同数据进行操作,导致数据的破坏、丢失。

InnoDB锁类型概述

InnoDB 锁.png

简介:

  1. 乐观锁与悲观锁是两种并发控制的思想,可用于解决丢失更新问题:
    1. 乐观锁会“乐观地”假定大概率不会发生并发更新冲突,访问、处理数据过程中不加锁,只在更新数据时再根据版本号或时间戳判断是否有冲突,有则处理,无则提交事务;
    2. 悲观锁会“悲观地”假定大概率会发生并发更新冲突,访问、处理数据前就加排他锁,在整个数据处理过程中锁定数据,事务提交或回滚后才释放锁;
  2. InnoDB 支持多种锁粒度,默认使用行锁,锁粒度最小,锁冲突发生的概率最低,支持的并发度也最高,但系统消耗成本也相对较高;
    所谓“行锁的系统消耗成本高”,是指在判断是否有行锁的时候,需要遍历各行。【?】
  3. 共享锁与排他锁是 InnoDB 实现的两种标准的行锁;
  4. InnoDB 有三种锁算法——记录锁(Record Locks)、间隙锁(Gap Locks)、还有结合了记录锁与间隙锁的临键锁(Next-Key Locks),InnoDB 对于行的查询加锁是使用的是“临键锁”这种算法,一定程度上解决了幻读问题;
    所谓“临键锁解决幻读”,主要是其会对记录,以及记录相关的间隙加锁,使其间隙不能被插入,所以一定程度上避免了幻读。
  5. 意向锁是为了支持多种粒度锁同时存在;
    意向锁本身是行级锁,其目的是表明有人正在锁定表中的行,或者打算锁定表中的行。
    在获取行锁之前先获取意向锁,是为了避免判断行锁时的遍历,即上述的“行锁的系统消耗成本高”问题。
    【意向锁在“意向锁(Intention Locks)【表级锁】”中以及理解很清楚了】

行锁实现:共享锁(即“读锁”)与排他锁(即“写锁”)

InnoDB 默认使用行锁,实现了两种标准的行锁——共享锁与排他锁。如下:

InnoDB 行锁实现:共享锁、排他锁.png

注意:

  1. 除了显式加锁的情况,其他情况下的加锁与解锁都无需人工干预。
  2. InnoDB 所有的行锁算法都是基于索引实现的,锁定的也都是索引或索引区间


共享锁与排它锁兼容性示例(使用默认的“REPEATABLE READ”隔离级别,图中数字从小到大标识操作执行先后顺序):

InnoDB 共享锁与排它锁兼容性示例.png

当前读(锁定读)与快照读(不加锁读)

  1. 快照读(不加锁读):读取记录的快照版本而非最新版本,通过MVCC实现。
    InnoDB 默认的“REPEATABLE READ”事务隔离级别下,不显式加“lock in share mode”与“for update”的“select”操作都属于快照读,保证事务执行过程中只有第一次读之前提交的修改和自己的修改可见,其他的均不可见。其中:
    1. “REPEATABLE READ”,同一事务中的所有一致读取将读取由该事务中的第一个此类读取构建的快照。
    2. “READ COMMITTED”,事务中的每个一致读取都会设置并读取其自己的新快照。
  2. 当前读(锁定读):读取记录的最新版本,会加锁保证其他并发事务不能修改当前记录,直至获取锁的事务释放锁。
    使用当前读的操作主要包括:显式加锁的读操作与插入/更新/删除等写操作,如下所示:
    select * from table where ? lock in share mode;
    select * from table where ? for update;
    insert into table values ();
    update table set ? where ?;
    delete from table where ?;
    
    注:当 Update SQL被发给MySQL后,MySQL Server会根据 where 条件,读取第一条满足条件的记录,然后 InnoDB 引擎会将第一条记录返回,并加锁,待MySQL Server收到这条加锁的记录之后,会再发起一个 Update 请求,更新这条记录。一条记录操作完成,再读取下一条记录,直至没有满足条件的记录为止。因此,Update 操作内部,就包含了锁定读。同理,Delete 操作也一样。Insert 操作会稍微有些不同,简单来说,就是 Insert 操作可能会触发 Unique Key 的冲突检查,也会进行一个当前读。
    其中,显示加锁的两种方式:
    1. “SELECT ... LOCK IN SHARE MODE”:
      在读取的任何行上设置共享锁。其他会话可以读取行,但是在事务提交之前不能修改它们。
    2. “SELECT ... FOR UPDATE”:
      对于搜索到的索引记录设置排他锁,锁定行和任何关联的索引条目。(如同为这些行发出“UPDATE”语句一样)

MVCC

MVCC,即“多版本并发控制”,与之对应的是“基于锁的并发控制”;【之前了解了MVCC,但并不清楚其实现】

InnoDB 默认的“REPEATABLE READ”隔离级别下,不显式加“lock in share mode”与“for update”的“select”操作都属于快照读。使用 MVCC,保证事务执行过程中只有第一次读之前提交的修改和自己的修改可见,其他的均不可见;


MVCC 的最大好处:读不加任何锁,读写不冲突,对于读操作多于写操作的应用,极大的增加了系统的并发性能;


关于 InnoDB MVCC 的实现原理,在《高性能MySQL》一书中有一些说明,网络上也大多沿用这一套理论:

  • 这套理论与 InnoDB 的实际实现还是有一定差距的,但可通过它初步理解 MVCC 的实现机制。
    【简而言之,就是对表添加两个隐藏的列:“当前版本号”、“删除版本号”。通过比对不同版本号,实现不加锁读(快照读)】
InnoDB MVCC的实现机制.png


MVCC 只适用于隔离级别中的读“Read committed”和“Repeatable Read”。不适用于“Read Uncommitted”,原因是 MVCC 的创建版本和删除版本只要在事务提交后才会产生。【?】

行锁算法:记录锁(Record Locks)、间隙锁(Gap Locks)、临键锁(Next-Key Locks)

InnoDB 主要实现了三种行锁算法(而并非锁的具体实现):

  • InnoDB 所有的行锁算法都是基于索引实现的,锁定的也都是索引或索引区间
InnoDB 锁算法:记录锁(Record Locks)、间隙锁(Gap Locks)、临键锁(Next-Key Locks).png
  1. 记录锁(Record Locks):
    顾名思义,记录锁就是为某行记录(索引记录)加锁。也称“索引记录锁”。
    • 记录锁始终锁定索引记录,即使没有定义索引的表也是如此。在这种情况下,InnoDB 将创建一个隐藏的聚集索引,并将该索引用于记录锁定。
    • 行级锁实际上是索引记录锁
  2. 间隙锁(Gap Locks):
    范围条件而不是相等条件检索数据时,对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB 也会,这种对这个“间隙”加锁的机制就是所谓的间隙锁(Next-Key锁)。
    • 间隙锁可以防止会阻塞符合条件范围内键值的并发插入,可以一定程度上防止“幻读”,但不可避免地造成一定的锁等待。
  3. 临键锁(Next-Key Locks):
    临键锁是索引记录上的“记录锁”和“索引记录之前的间隙上的间隙锁”的组合。
    • 默认情况下,InnoDB 以“REPEATABLE READ”事务隔离级别运行。在这种情况下,InnoDB 使用“临键锁”进行搜索和索引扫描,这可以防止幻读。【!!!】

事务隔离级别 与 行算法

不同的事务隔离级别、不同的索引类型、是否为等值查询,使用的行锁算法也会有所不同。


下面仅以 InnoDB 默认的“REPEATABLE READ”隔离级别、等值查询为例,介绍几种行锁算法:【???】

  • 【InnoDB 表是索引组织表,根据主键索引构造一棵 B+ 树,叶子节点存放的是整张表的行记录数据,且按主键顺序存放。】
InnoDB “REPEATABLE READ”隔离级别下的等值查询的锁内容.png
  1. “等值查询”使用“聚簇索引”:
    InnoDB “REPEATABLE READ”隔离级别下:“等值查询”使用“聚簇索引”.png
    表格模拟主键索引的叶子节点,使用主键索引查询,就会锁住相关主键索引,锁住了索引也就锁住了行记录,其他并发事务就无法修改此行数据,直至提交事务释放锁,保证了并发情况下数据的一致性;
  2. “等值查询”使用“唯一索引”:
    InnoDB “REPEATABLE READ”隔离级别下:“等值查询”使用“唯一索引”.png
    辅助索引的叶子节点除了存放辅助索引值,也存放了对应主键索引值;锁定时会锁定辅助索引与主键索引;
  3. “等值查询”使用“辅助索引”:
    InnoDB “REPEATABLE READ”隔离级别下:“等值查询”使用“辅助索引”.png
    间隙锁,锁定的是索引记录之间的间隙,是防止幻读的关键
    如果没有上图中绿色标识的间隙锁,其他并发事务在间隙中插入了一条记录如:“insert into stock (id,sku_id) values(2,103);”并提交,那么在此事务中重复执行上图中 SQL,就会查询出并发事务新插入的记录,即出现幻读;(幻读是指在同一事务下,连续执行两次同样的 SQL 语句可能导致不同的结果,第二次的 SQL 语句可能返回之前不存在的行记录)加上间隙锁后,并发事务插入新数据前会先检测间隙中是否已被加锁,防止幻读的出现;

锁问题

MySQL 锁会带来如下几种问题,如果能解决他们,就可以保证并发情况下不会出现问题:

锁问题 问题描述 相关隔离级别 解决办法
脏读 一个事务中会读到其他并发事务未提交的数据; “Read Uncommitted” 提高事务隔离级别至“Read Committed”及以上;
不可重复读 一个事务会读到其他并发事务已提交的数据(“update”和“delete”的数据);

“在一个事务生命周期内,多次执行同样的 SQL 语句,可能得到的数据内容不一致。”

“Read Uncommitted”、“Read Committed” 解决办法分为两种情况:
  1. 当前读(锁定读):使用临键锁(Next-Key Locks)机制对相关索引记录及索引间隙加锁,防止并发事务修改数据或插入新数据到间隙;
  2. 快照读(非锁定读):MVCC,保证事务执行过程中只有第一次读之前提交的修改和自己的修改可见,其他的均不可见;
幻读 一个事务会读到其他并发事务已提交的数据(“insert”的数据);

“在一个事务生命周期内,多次执行同样的 SQL 语句,可能得到的数据数量不一致。”

“Read Uncommitted”、“Read Committed”、“Repeatable Read” 默认的“REPEATABLE READ”隔离级别下,不可能出现“幻读”。【因为“临键锁”(当前读)、“MVCC”机制(快照读)的优化】
  • ANSI SQL隔离级别标准里可重复读级别是存在幻读问题。

其他隔离级别下,解决办法分为三种情况:

  1. 当前读(锁定读):
    1. 使用临键锁(Next-Key Locks)机制对相关索引记录及索引间隙加锁,防止并发事务修改数据或插入新数据到间隙;
    2. 手动加行排他(X)锁:“SELECT...FOR UPDATE”;
  2. 快照读(非锁定读):MVCC,保证事务执行过程中只有第一次读之前提交的修改和自己的修改可见,其他的均不可见;
    • 【仅读取版本号小于当前版本号的数据(新插入的数据版本号大于当前版本号)】
  3. 提高事务隔离级别至“Serializable”;
    • 完全串行化的读,每次读都需要获得表级共享锁,读写相互会相互互斥,会大大的降低数据库的实际吞吐性能。
丢失更新 一个事务提交更新被另一个事务提交的更新所覆盖。

两个事务 A、B 对同一数据先后提交更新,而后提交的事务 B 未在事务 A 对数据更改的基础上进行更新(即:事务 A 的更新被冲掉,谓之“丢失事务 A 的更新”)

“Read Uncommitted”、“Read Committed”、“Repeatable Read” 默认的“REPEATABLE READ”隔离级别下 ,解决办法分为两种情况:
  1. 乐观锁:数据表增加 version 字段,读取数据时记录原始 version,更新数据时,比对 version 是否为原始 version,如不等,则证明有并发事务已更新过此行数据,则可回滚事务后重试直至无并发竞争;
  2. 悲观锁:读加排他锁,保证整个事务执行过程中,其他并发事务无法读取相关记录,直至当前事务提交或回滚释放锁;
  • 不可重复读重点在于“update”和“delete”,而幻读的重点在于“insert”。
  • MVCC 可以解决不可重复读、幻读。
  • 默认的“REPEATABLE READ”隔离级别下,不可能出现“幻读”。【因为“临键锁”(当前读)、“MVCC”机制(快照读)的优化】