摘要:本文学习了数据库常见的锁。
环境
CentOS Linux release 7.6.1810
MySQL 5.7.40
1 简介
1.1 概述
数据库中的锁是为了在并发场景下保证一致性而设计的规则。
不同的存储引擎支持不同的锁机制,InnoDB支持行锁和表锁,MyISAM支持表锁。
1.2 说明
1.2.1 共享锁和排他锁
按锁的互斥程度来分,可以分为共享锁和排他锁。
1.2.1.1 共享锁
共享锁(Share Locks),记为S锁,InnoDB和MyISAM都有,MyISAM只能加在表上,InnoDB可以加在表上和行上。
共享锁也称为读锁,多个读操作可以同时进行,其他写操作会被阻断。
1.2.1.2 排他锁
排他锁(Exclusive Locks),记为X锁,InnoDB和MyISAM都有,MyISAM只能加在表上,InnoDB可以加在表上和行上。
排他锁也称为写锁,其他读操作和写操作会被阻断。
1.2.2 表锁和行锁
按锁的粒度来分,可以分为表锁和行锁。
1.2.2.1 表锁
表锁是粒度最大的锁,表示当前操作对整张表加锁,资源开销小,不会出现死锁,锁冲突概率大。InnoDB和MyISAM都支持。
MyISAM在执行查询语句前,会给涉及的表加读锁,在执行增删改操作前,会给涉及的表加写锁。
InnoDB存储引擎不会为表添加表级别的读锁或者写锁。在对表进行DDL操作时其他事务对表的增删改查操作会被阻塞,在对表进行增删改查操作时,其他事务对表进行DDL操作也会被阻塞,这个过程其实是通过在Server层使用一种称之为元数据锁(Meta Data Locks,简称MDL锁)结构来实现的。
1.2.2.2 行锁
行锁是粒度最小的锁,表示当前操作对操作的行加锁,资源开销大,会出现死锁,锁冲突概率小。只有InnoDB支持。
1.3 优先级
当锁被释放时,优先执行写锁队列中的请求,写锁队列为空时,执行读锁队列中的请求。
所以,如果有大量更新操作,可能会导致查询操作很难获得锁,从而长久阻塞,使程序响应超时。
2 使用
2.1 表锁
2.1.1 逻辑
当前线程给当前表加S锁,当前线程只能读取当前表,当前线程更新当前表失败,当前线程查询其他表失败,其他线程只能读取当前表,其他线程更新当前表阻塞。
当前线程给当前表加X锁,当前线程可以读取和更新当前表,当前线程查询其他表失败,其他线程读取当前表阻塞。
2.1.2 操作
查看表上的锁:
1 | show open tables where in_use > 0; |
在执行查询语句时会自动给表加读锁,也可以显示加锁:
1 | lock tables 表名 read; |
在执行更新语句时会自动给表加写锁,也可以显示加锁:
1 | lock tables 表名 write; |
释放锁:
1 | unlock tables; |
2.2 意向锁
2.2.1 说明
InnoDB为了支持多粒度锁机制,即为了支持表锁和行锁共存,引入了意向锁(Intention Locks)。
意向锁属于表级锁,主要是为了处理行锁和表锁之间的冲突,事务在请求读锁和写锁之前,需要先获得对应的意向共享锁和意向排他锁。
使用意向锁提高了在多粒度锁并存时判断某行数据是否存在行锁的性能:
- 在没有使用意向锁的情况下,线程直接使用行锁后,其他线程在使用表锁前,需要判断每行数据是否有行锁。
- 在使用意向锁的情况下,线程在使用行锁前需要先给表加IS锁或IX锁,其他线程在使用表锁前,只需要判断IS锁和IX锁即可,不需要判断每行数据。
意向锁分为IS锁和IX锁:
- 意向共享锁(Intention Share Locks),记为IS锁,表示事务有意向对表中的某行加共享S锁。
- 意向排他锁(Intention Exclusive Lock),记为IX锁,表示事务有意向对表中的某行加排他X锁。
2.2.2 逻辑
当前线程加IS锁并且给当前行加行锁,其他线程可以加IS锁并且给当前行加行锁,其他线程可以加IX锁并且给其他行加行锁,其他线程可以加S锁,其他线程不能加X锁。
当前线程加IX锁并且给当前行加行锁,其他线程可以加IS锁并且给当前行加行锁,其他线程可以加IX锁并且给其他行加行锁,其他线程不能加S锁,其他线程不能加X锁。
2.2.3 操作
查询时加意向共享锁:
1 | select * from 表名 lock in share mode; |
查询时加意向排他锁:
1 | select * from 表名 for update; |
2.2.4 注意
InnoDB行锁是针对索引加的意向锁,不是针对记录加的意向锁。
意向锁在某些情况下不会加行锁:
- 查询语句没有使用索引,此时会使用表锁。
- 查询语句使用范围条件导致索引失效,此时会使用表锁。
- 查询语句使用索引但没有查到数据,此时不会使用任何锁。
2.3 元数据锁
2.3.1 说明
元数据锁(Meta Data Lock,简称MDL锁),表锁,在5.5版本后引入,目的是为了保证读写的正确性。
2.3.2 逻辑
当前线程对表做增删改查操作时加MDL读锁
当前线程对表做结构变更操作时加MDL写锁。
2.4 自增锁
2.4.1 说明
自增锁(Auto-inc Locks),表锁,特殊的表锁,发生在AUTO_INCREMENT主键自增的列。
2.4.2 逻辑
多事务同时插入时会阻塞,保证主键的连续。
2.5 记录锁
2.5.1 说明
记录锁(Record Locks),行锁,索引记录锁,建立在索引记录上的锁,为某行记录加锁,索引基于唯一索引(包括主键索引)。
2.5.2 逻辑
当前线程给当前行加S锁后,其他事务也可以对当前行加S锁,但不可以对当前行加X锁。
当前线程给当前行加X锁后,其他事务不可以对当前行加S锁,也不可以对当前行加X锁。
2.5.3 操作
查询时加共享记录锁:
1 | select * from 表名 where id = 1 lock in share mode; |
查询时加排他记录锁:
1 | select * from 表名 where id = 1 for update; |
2.6 间隙锁
2.6.1 说明
间隙锁(Gap Locks),行锁,索引区间锁,建立在索引区间上的锁,为某个开区间加锁,开区间不包括端点。
使用间隙锁可以防止幻读,保证索引区间上的数据不会被更改,只在默认RR可重复读级别有效,降级后自动失效。
2.6.2 操作
查询时加共享间隙锁:
1 | select * from 表名 where id > 1 lock in share mode; |
查询时加排他间隙锁:
1 | select * from 表名 where id > 1 for update; |
2.7 临键锁
2.7.1 说明
临键锁(Next-key Locks),行锁,记录锁和间隙锁的组合,索引区间是一个开区间,索引基于非唯一索引。临键锁是InnoDB默认的锁。
每个数据行都可以看做一个记录锁,数据行上下相邻两边的间隙可以看做两个间隙锁。使用临建锁也可以防止幻读,保证索引区间上的数据不会被更改,也只在默认RR可重复读级别有效。
2.7.2 逻辑
临键锁在使用唯一索引的不同场景中会变化:
- 使用唯一索引,使用精确匹配,记录存在,变化为记录锁。
- 使用唯一索引,使用精确匹配,记录不存在,变化为间隙锁。
- 使用唯一索引,使用范围匹配,变化为临键锁,左开右闭。
- 没有使用索引,变化为表锁。
2.8 插入意向锁
2.8.1 说明
插入意向锁(Insert Intention Locks),行锁,特殊的间隙锁。
多个事务同时在同一个间隙中插入记录时,获取各自的插入意向锁,不同行的插入不会冲突。
2.8.2 逻辑
插入意向锁并不会阻止别的事务继续获取该记录上任何类型的锁。
3 死锁
3.1 说明
死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环。
3.2 解决
3.2.1 等待超时
当两个事务互相等待时,当一个事务等待时间超过设置的阈值时,就将其回滚,另外事务继续进行。
可以通过如下命令查询阈值:
1 | mysql> show variables like 'innodb_lock_wait_timeout'; |
默认情况下,超过50秒未获取到锁的事务,会自动回滚并返回错误,同时会释放自己占有的锁资源。
在高并发场景中,等待50秒的时间太长了,会导致越来越多的事务阻塞。但是如果将等待阈值设置的过小,会导致正常等待锁资源但未发生死锁的事务超时报错。
3.2.2 死锁检测
因为等待超时具有缺陷,因此InnoDB提供了一种死锁检测机制,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。
死锁检测机制被启用后,会收集两个信息:
- 锁的信息链表:目前持有每个锁的事务。
- 事务等待链表:阻塞的事务要等待的锁。
死锁检测流程:
- 每当一个事务需要阻塞等待某个锁时,就会触发一次死锁检测算法,该算法会以当前事务作为起点,然后从锁的信息链表中找到持有目标锁的事务。
- 根据持有目标锁的事务,在事务等待链表中查找,看看这个事务是否在等待获取其他锁。
- 如果这个事务还在等待获取其他锁,则根据锁的信息链表找到持有其他锁的事务。
- 根据持有其他锁的事务,在事务等待链表中查找,看看这个事务是否在等待获取其他锁。
- 经过一系列的判断后,判断是否出现闭环,出现的话则证明出现了死锁现象,然后强制回滚其中的一个事务,来达到解除死锁的目的。
在选择事务回滚时,会根据回滚日志找到回滚量最小的事务进行回滚。
可以通过命令查看是否开启死锁检测机制:
1 | mysql> show variables like 'innodb_deadlock_detect'; |
虽然死锁检测机制能够更合理的解决死锁问题,但实际上死锁检测的开销不小,当阻塞的并发事务越来越多时,检测的效率会呈线性增长。
3.3 监控
使用命令监控数据库中锁的等待信息:
1 | mysql> show status like 'innodb_row_lock%'; |
说明:
- Innodb_row_lock_current_waits:当前正在等待锁的个数。
- Innodb_row_lock_time:从系统启动到现在等待锁的总时长。
- Innodb_row_lock_time_avg:从系统启动到现在等待锁的平均时长。
- Innodb_row_lock_time_max:从系统启动到现在等待锁的最久时长。
- Innodb_row_lock_waits:从系统启动到现在等待锁的总个数。
条