专栏地址:
文章目录
1. 什么是锁
锁用于管理对共享资源的并发访问,不同的数据库有不同的锁设计。InnoDB支持行锁,MyISAM使用的是表锁,SQL Server还支持页锁。MySQL中的锁有全局锁、表锁、行锁(InnoDB)。
锁粒度
提高系统并发度的一种方式就是降低锁粒度,让锁住的对象更精准。同时,锁管理需要耗费系统资源。锁策略就是在并发安全性和系统开销之间作出平衡。
latch闩锁与lock锁
在InnoDB中:
latch用于保护内存中的数据结构,其加锁的对象是线程。
lock用于保护数据库的内容,比如表、页、行,其加锁的对象是事务。并且具有死锁检测与处理机制,包括超时获取以及利用waits-for graph等待图实现的主动检测。
2. 全局锁
定义
全局锁就是对整个数据库实例进行加锁,MySQL提供了全局对锁命令flush table with read lock
让整个数据库处于只读状态,阻塞DDL、DML等更新类语句。
应用场景
全局锁的典型应用场景是全库逻辑备份。
全库逻辑备份的目的是为了得到某一个时间点上的一个一致性的逻辑视图。
对于MyISAM这种不支持事务的引擎,只能使用全局锁来保证一致性。
而对于InnoDB,则可以在可重复读的隔离级别下开启一个事务来拿到一致性视图。MySQL自带的逻辑备份工具mysqldump,在导出数据之前会开启一个事务,确保拿到一致性视图,再加上MVCC的支持,整个过程中数据库是可以更新的。
3. 表级锁
MySQL中的表级别的锁有两种:表锁和元数据锁(meta data lock,MDL)。
3.1 表锁
MySQL在Server层实现了表锁,可以对表上加读锁或者写锁,语法是lock tables....read/write
。表锁开销小,在没有出现更细粒度的锁的时候,表锁是最常见的处理并发的方式,比如MyISAM就使用表锁。
3.2 元数据锁
3.2.1 作用与加锁规则
作用
元数据锁的作用是防止DDL和DML(CRUD)操作并发冲突,避免在读取或者更新表数据期间,表结构发生变更,导致数据无法对齐。
加锁规则
元数据锁不需要显式使用,在对一个表进行增删改查操作时会自动加上元数据读锁;在对表结构进行变更时会自动加上元数据写锁。
3.2.2 如何给表加字段
事务中的MDL锁会在语句执行的时候加锁,但要等到事务结束后才会释放锁,在做表结构变更的时候,容易导致锁住线上查询和更新。
比如,在如下场景:session C会被 session A阻塞,而由于session C的存在,其后的所有CRUD操作都会被阻塞。session B不会阻塞,并且由于没有在事务中,其读锁会立即释放。
![](https://i-blog.csdnimg.cn/blog_migrate/6b9b85aea4e37bca1a3fb09b971786d1.png#pic_center)
Online DDL 允许在表上执行 DDL 操作的同时减少阻塞并发的 DML(CRUD) 操作,防止锁表影响线上业务。
Online DDL—MySQL5.7
MySQL原生的Online DDL有两种算法:
- copy:创建一个新表并应用DDL,然后将数据拷贝过去。整个环节会阻塞写操作但不会阻塞读操作。
- in-place:在原表上进行重建,在准备和提交阶段会加元数据写锁,除此之外不会阻塞DML操作。但写请求不能超多设定的缓存大小。
Online DDL—PT工具
PT工具的原理:
- 创建一张与旧表具有相同表结构的新表。
- 在新表上执行DDL操作
- 在旧表上创建INSERT、UPDATE、DELETE三种类型触发器。
- 将旧表的数据拷贝到新表,同时通过触发器将旧表中的操作映射到新表。(在业务高峰期会极大家中主库的负载)
- 重命名新表并删除旧表。
4. 行锁
MySQL的行锁是由存储引擎自己实现的,并不是所有存储引擎都支持行锁,比如MyISAM。其并发控制只能使用表锁,同一张表同一个时刻只能有一个更新操作,影响业务并发度。
行锁是对数据库表中行记录的锁,InnoDB中的**行锁是作用在所使用到的索引树上(不看WHERE条件)**的, 通过锁住索引树中的行实现。
4.1 行锁算法
InnoDB中行锁有三种算法:
- Record Lock:单个行记录的锁,有两种类型:
- 共享锁(S):读锁
- 排他锁(X):写锁
- Gap Lock:间隙锁,锁定一个范围,但不锁定记录本身。间隙锁之间互不冲突,与间隙锁冲突的是往这个间隙里插入记录这个操作(插入意向锁),Gap Lock解决了幻读问题。
- Next-Key Lock:Record Lock + Gap Lock,是一个左开右闭区间的锁,即锁定一个范围也锁定记录本身。
在可重复读隔离级别下,InnoDB默认的行锁算法是Next-Key Lock。在特定情况下会退化为Record Lock,比如在索引树上进行等值查询,并给唯一索引加锁的时候;也有可能退化为Gap Lock,比如等值查询没用命中时,会在其两侧加上Gap Lock。
在提交读隔离级别下,行锁算法是Record Lock。
Gap Lock与幻读
幻读是指在同一个事务中,对同一范围的查询先后读取到了不同的数据集合,即其中一次查询读取到了另一次查询没有的记录(发生了INSERT)。在可重复读的隔离级别下,普通查询是快照读,并不会出现幻读,只有当前读才可能会出现。
由于普通的Record Lock锁住的是记录,并不能防止在“间隙“插入这个操作,所以InnoDB引入了Gap Lock。Gap Lock在可重复读隔离级别下才会生效。
Gap Lock的引入,会导致同样的语句锁住更多的范围,影响并发度。
因此,与标准的SQL隔离级别不同的是,InnoDB在可重复读级别下,利用Next-Key Lock解决了幻读问题,能够完全保证事务的隔离性,达到了串行化级别。
4.2 两阶段锁
由于数据库事先不知道会访问到哪些数据,无法对使用到的数据进行一次性加锁。所以,在InnoDB中行锁是在需要的时候,在查找过程中访问到相应的行时才会进行加锁。但并不会立即释放,而是要到事务结束的时候在进行统一进行释放。这就是两阶段锁协议,逐行加锁、统一释放。
![](https://i-blog.csdnimg.cn/blog_migrate/682229797224578b7094ced0a8b94d65.png#pic_center)
图中,事务B会被阻塞,直到事务A提交释放行锁后。
指导意义
如果事务中需要锁定多个行,把最有可能造成锁冲突、影响并发度的锁尽量往后放,降低这个锁的持有时间,最大程度的减少事务之间的锁等待,提高并发度、降低死锁概率。
4.3 加锁规则
4.3.1 规则
在MySQL5.7默认可重复读的隔离界别下,行锁的加锁规则是:
- 行锁的默认算法是Next-Key Lock,是一个左开右闭的区间,锁住当前记录及其左区间。
- 锁是在需要的时候,在查找过程中访问到相应的行时才会进行加锁。
- 在索引树上进行等值查询时(即通过B+树定位到页,再通过页内的稀疏目录定位到行的过程),若加锁的对象是唯一索引,则Next-Key Lock会退化为Record Lock;若查询条件没有命中行,则Next-Key Lock退化为Gap Lock。
- 在索引树上进行等值扫描时(通过链表顺序访问叶子节点行记录),行锁算法是默认的Next-Key Lock,若扫描终止时最后一个记录不满足条件时,则Next-Key Lock退化为Gap Lock。
- 在索引树上进行范围扫描时,行锁不退化。
- Next-Key Lock加锁顺序:Gap Lock + Record Lock
PS:
对于使用到的索引,在索引树上访问到的行均需要加锁,不看WHERE条件。
使用二级索引时,若需回表,则还需要在聚集索引上进行加锁。
4.3.2 例子
CREATE TABLE t (
ìd ìnt(11) NOT NULL,
c ìnt(11) DEFAULT NULL,
d ìnt(11) DEFAULT NULL,
PRIMARY KEY (id),
KEY c (c)
) ENGINE=InnoDB;
insert into t values(0,0,0),(5,5,5), (10,10,10),(15,15,15),(20,20,20),(25,25,25);
等值查询未命中时加间隙锁
id = 7的等值查询的记录不存在,那么Next-Key Lock会退化为Gap Lock,锁住区间(5, 10)。
![](https://i-blog.csdnimg.cn/blog_migrate/dfc7862c6596afe115ff54c80fbcfd45.png#pic_center)
覆盖索引只锁辅助索引、普通索引需额外加一间隙锁
![](https://i-blog.csdnimg.cn/blog_migrate/9fe098214ba8699807f5fd4d29d11090.png#pic_center)
覆盖索引由于不会需要回表,所以只会在辅助索引上进行加锁而不会对聚集索引进行加锁,除非是FOR UPDATE。
普通索引,在等值查询找到第一个满足条件的记录后,由于索引不唯一,还需继续扫描。此时,扫描到的行都需要加上Next-Key Lock,由于最后一个扫描到的行不满足查询条件,所以退化为了Gap Lock。
PS:利用覆盖索引+LOCK IN SAHRE 无法防止数据被更新,使用FOR UPDATE 或者打破覆盖索引。
全表扫描逐行加锁
SELECT * FROM t WHERE a = 5 FOR UPDATE
,若a列没有索引导致全表扫描时,会在聚集索引树上逐行扫描、逐行加锁(Next-Key Lock),既加上了Record Lock,也在记录两侧的间隙加上了Gap Lock,确保无法插入新纪录,然后在事务提交时统一释放。
SQL范围查找 = 索引树 等值查找 + 范围扫描
![](https://i-blog.csdnimg.cn/blog_migrate/5eb121bf11ed7b2e1c2021c6342cd2cb.png#pic_center)
首先,在索引树上进行等值查询以定位到 id = 10的记录,在唯一索引上树上进行等值查询时,Next-Key Lock退化为Record Lock,所以只加了 id = 10的Record Lock。
随后,在索引树上进行范围扫描,锁不退化,加(10, 15]的Next-Key Lock。
普通索引存在重复值:行的概念,逐行加锁
假设在索引c有如下数据,其中 c = 10 有两条记录:
![](https://i-blog.csdnimg.cn/blog_migrate/2f1a1fc80b5cc8f27a8723ffdfa04240.png#pic_center)
SELECT id FROM t WHERE c = 10 IN SHARE MODE
这条SQL的加锁过程是:
首先等值查询定位到第一条c = 10的记录,加上(c=5,id=5) 到 (c=10,id=10) 的Next-Key Lock。
随后,向后等值扫描下一行记录,加上(c=10,id=10)到(c=10,id=30)的Next-Key Lock;再向后扫描下一行记录,由于碰到了不满足条件的行,加上(c=15,id=15)的Gap Lock。
所以,总结起来的加锁范围为:(c=5,id=5) 和 (c=15,id=15)的开区间锁。
![](https://i-blog.csdnimg.cn/blog_migrate/05d5afbb7603254b58efd098b1b05e43.png#pic_center)
LIMIT语句加锁:扫描及时终止
Next Lock加锁顺序:先加间隙锁,再加Record Lock
由于加锁顺序,下面的情况将会产生死锁。
![](https://i-blog.csdnimg.cn/blog_migrate/983a4282fe9c5973dda6c581c19330c9.png#pic_center)
首先,session A在索引 c 上加了 (5,10] 的Next-Key Lock 和 (10,15) 的Gap Lock。
随后,Session B在加(5,10] 的Next-Key Lock时,由于间隙锁之间不互斥,先加上了(5,10) 的Gap Lock,然后尝试加 c = 10的Record Lock时被阻塞住。
随后,session A的INSERT操作被session B的Gap Lock阻塞。
两者出现了循环等待,产生了死锁。
ORDER BY DESC
会先查找最大值。
4.4 死锁的检测与处理
并发系统中不同线程出现资源循环依赖并进入无限等待的状态,称之为死锁,即互相持有对方所需要的锁。
InnoDB主要采用两种方式来预防死锁:超时获取+基于等待图的主动检测。
产生死锁的四个必要条件:
- 互斥:独占性,一个资源同一时间只能被一个线程所拥有。
- 不可抢占:在资源未被使用完毕前,其它申请者不能强行剥夺。
- 请求与保持:进程保持已经获得的资源,去请求其它资源并因此而阻塞。
- 循环等待:若干线程想成了首尾相接的循环等待资源关系。
超时获取
当获取锁的等待超过一定时间时,自动退出等待并回滚事务。超时获取的主要缺点在于时间阈值不好确定。
基于等待图(wait-for graph)的主动检测
根据事务所持有的锁、以及尝试获取锁的信息,绘制事务之间的等待图,若图中存在回路,则说明存在死锁。此时,InnoDB会主动回滚undo量最少的事务。
等待图是一种主动检测策略,在每个事务请求锁并发生等待时,均会将其放入等待图中,并判断是否会产生回路。
InnoDB采用深度优先算法对等待图进行回路检测。
等待图的缺点在于会耗费较多的CPU资源。
解决热点更新所造成的死锁
控制并发度,以降低等待图检测的CPU消耗。
并发度控制可在中间件或者MySQL服务端实现。
间隙锁导致的死锁
Gap Lock解决了幻读问题,但也带来了一些困扰,可能会导致死锁。比如,采用先查询再插入的方式进行插入防重时,若数据库没有待插入记录,那么两个事务分别对同一个范围加锁,并获得了间隙锁,然后再分别尝试向这个间隙插入数据,就会导致死锁。下面的例子中,数据库记录0 5 10 15。
![](https://i-blog.csdnimg.cn/blog_migrate/de32d6a6af8ac136f498b3c52caaf4a3.png#pic_center)
Session A获得了(5, 10)间隙锁,由于间隙锁之间不互斥,Session B也获得了间隙锁。随后,Session B的insert操作被Session A的间隙锁阻塞,Session A的insert操作被Session B的间隙锁阻塞。两个事务进入了互相等待状态,形成死锁。
参考
《高性能MySQL》
《MySQL实战45讲》
《MySQL技术内幕(InnoDB存储引擎)》