本篇还是从设计者的视角出发,对锁相关的知识点进行逻辑梳理。
innodb 的锁这块内容,一篇文章是没法说全面的。本文从理论角度来说明,更多关于加锁分析的内容,可以网上搜索关键字,如:innodb 加锁分析。不过,在后面的优化实战部分,会结合实际案例对加锁过程进行分析。
首先要理解为什么会出现锁这类解决方法,它解决的是什么问题?
解决的是 并发 的竟态问题。即对多个执行者针对某一资源的操作,最终的执行结果和访问该资源的特定顺序有关。
假如有十个事务都去读取同一行记录,虽然是并发场景,但是不会产生竟态问题。因为最终的执行结果和访问该资源的顺序无关,不论哪个事务先读取记录,都对最终结果没有影响。
但是假如事务A执行当前读,事务B修改事务A需要读取的记录行。那么事务最终的执行结果,取决于事务B的修改发生的时机。这就产生了竟态问题(假设未引入锁机制)。
那么,针对并发的竟态问题,解决思路就是:保证操作的原子性和语义的一致性
基于这个思路,有两种解决方案:
- 加锁
- 无锁
先来看加锁方案。当执行者A给资源加锁后,其他执行者访问该资源时,看到有锁存在,则需要等待执行者A操作完成后才能继续执行。
此处的问题在于,加锁的过程如何保证原子性,即如何避免执行者A加锁过程中,执行者B也对资源进行加锁呢?这里的实现需要底层CPU指令的配合(当然也有其他解决方案)。
锁
同样是存储在内存中的一个数据块,给资源加锁,也就是将这个锁数据块与资源关联起来。
加锁的解决方案中,有两大块是优化的核心,即:
- 锁住的范围
- 锁对资源的保护性
那么无锁,即 Lock Free
,同样是并发编程中非常重要的话题。但是无锁并不代表没有 锁
,只是将锁的粒度降到最小,也就是锁住的区域最小,这样冲突的概率也最小。所以无锁更为高效。
Mysql 8 之前的版本在redo log这块的操作采用的就是方案1,即加锁实现;而mysql 8 开始,这块底层实现就改为了方案2,即无锁实现。不过对开发者没有影响。
有了上述铺垫,我们就来看看innodb中的锁,将从以下四点进行说明:
- 锁类型
- 锁协议
锁
这个实体是什么- 死锁问题
对上述四点有了解即可,这里我们只针对重点锁进行说明。
首先来看锁类型,分成两大类:表锁和行锁。
表锁又可划分为:
- MDL锁
- 表锁
- 表级意向锁
其中,MDL锁就是元数据锁,比如修改数据表结构时会加锁,属于mysql server层的锁。而Innodb中的表锁基本用不到,可以认为没有表锁。表级意向锁我们等会接着说。
而行锁顾名思义,就是针对数据行的锁。范围小,重量轻,性能高。innodb的行锁从下面两个方面归纳:
- 行锁的三种算法
- 行锁升级
其中,行锁的三种算法分别为:record lock、gap lock和 next-key lock。在接着往下走之前,需要知道一个知识点:
行锁是加在索引上的,即加在聚簇索引或者非聚簇索引上。
record lock 即锁住某一个数据行,gap lock 即锁住数据行之间的间隙,比如主键 id 分别为2,5,6,则 gap 锁可能加在 (2,5)之间,即 id 区间 (2,5)之内,无法插入数据,比如id=4就无法插入。
而 next-key lock 就是 record lock + gap lock 的集合,比如扫描到 id=5 ,则加锁区间为 (2,5]。
再来看官方的描述:
locking read, an
UPDATE
, or aDELETE
generally set record locks on every index record that is scanned in the processing of the SQL statementThe locks are normally next-key locks that also block inserts into the “gap” immediately before the record
描述告诉我们两件事:
- 扫描的索引记录都会加锁
- 加的锁是 next-key lock
我们知道一个数据页中存储了很多的数据行,如果其中的很多数据行都有锁,那么这么锁肯定会消耗很多的系统资源,所以当一个数据页中数据行很多都有锁时,我们可以将其升级为一个统一的页锁。这就是锁升级。
但是…innodb有着更加巧妙的处理方法,使得不论页中数据行有多少加锁,都和一行加锁消耗的系统资源一样。
实际上,锁
这个实体就是内存中的一个数据结构,可以类比为数据页这种数据结构。比如锁这个对象实体具有的属性有:
- 锁类型
- 事务ID
- 记录行位图
- 等等
其中,记录行位图就可以用来标记这个数据页中哪些行被加锁,得以节省系统资源。当执行加锁这个动作时,实际上就是修改内存中的锁结构,并将其和数据行关联起来。
知道了锁的类型和实体概念后,我们再来看下锁协议。锁协议即我们常听说的 两阶段锁
,即加锁和解锁是两个不同的阶段。即执行加锁语句是加锁,但是语句执行完不会立马释放,而是等到事务提交或者回滚时才解锁释放。
不论表锁还是行锁,都可以分为共享锁(Share lock)和排它锁((exclusive lock),共享锁简写为S锁,排它锁简写为X锁。
读操作加共享锁,写操作加排它锁。要解决竟态问题,那么S和X锁相互之间肯定是互斥的。所以id=5的数据行加了S锁,就无法加X锁,反之亦然。但是可以继续加相同的S锁。
表中记录加上行锁后,假如此时要给表加表锁,那么肯定要检查表中记录行是否存在行锁。遍历是不可能遍历的,同样可以参考锁结构位图的思想。在加行锁的时候,给表加上意向锁,即分别为:IS锁、IX锁。
意向锁的作用仅仅是标识该表中的记录行正存在S锁或者X锁,或者没有记录行加锁。这样加表锁的时候就不需要遍历检查记录行了。
当给记录行加S锁时,会给表加IS锁,这样当再准备向表加表级X锁时,该操作就会被阻塞。当时给表加表级S锁时,该操作就可以继续执行。
最后一块是死锁,死锁无关乎数据库,并发场景都可能遇到。死锁问题的解决方案分两种:
- 超时放弃
- 检测环
死锁问题简单了解即可,涉及到加锁分析后可以再进一步讨论。
关于锁这块,重点就是加锁分析,虽然扫描到的锁都会加 next-key lock,但是针对不同的索引类型和查找条件,next-key lock会退化为record lock 或者 gap lock,或者扫描的行数不同。相关的知识内容,在实战篇中再进一步分析,先把头脑中的逻辑和知识脉络梳理清楚。
来源:oschina
链接:https://my.oschina.net/u/4408053/blog/4294939