Mysql(2)-锁总结
2-1.MVCC和Next-key locks
2-1.1 MVCC(MultiVersion Concurrent Control)
2-1.1.1 机制
MVCC即多版本并发控制,是InnoDB自带的一种机制,其为表格添加了三个列,如下
DB_ROW_ID | DB_ROLL_PTR | DB_TRX_ID | col1 |
---|---|---|---|
隐藏的行id,当没有合适的列可以创建索引时,使用此列创建索引 | 回滚指针,指向Undo Log中的上一版本数据的地址 | 记录当前事务的id |
其中DB_TRX_ID和DB_ROLL_PTR是关键,下面举例说明,
当一个事务A开启后,在RR隔离级别下,只在第一次调用select时会获得一个表格的Read View,其包括四个属性
- trx_ids: 截至目前,系统活跃的事务版本号,包括自己
- low_limit_id: 当前事务版本号+1
- up_limit_id: 除本事务外,截至目前,活跃事务的最小版本号
- creator_trx_id: 创建当前read view的事务版本号
那么在开启事务A后,执行
-
SELECT
会根据Read View对数据进行过滤,其可以读取到的数据有如下部分
- 本事务内添加的数据
- 所有版本号小于up_limit_id的已提交事务的结果
除上面提到的,都不可以select到,如trx_ids中除了当前事务号插入的数据,因为这些事务仍然活跃且未提交
-
写
包括update、insert、delete,都会将对应数据行的DB_TRX_ID赋值为当前事务版本号
2-1.1.2 当前读和快照读
1)快照读
MVCC机制下,select读取数据是不需要加锁的,其读取的是快照中的数据,包含事务开启前的数据以及当前事务添加的数据
2)当前读
也即insert、update、delete这三种方式,其会对访问数据加锁
见下面例子
事务A | 事务B |
---|---|
begin; | |
begin; | |
select * from test; 🎈 | |
insert into test(id,name) values(3,'kk'); | |
select * from test;与🎈处查询结果一致 | |
commit; | |
insert into test(id,name) values(3,'kk'); 主键冲突报错!因为此数值已经出现在表格中,但是由于查询的是快照,因而不能查询出事务B的结果;如果在事务Bcommit之前发生,则会被阻塞 | |
select * from test;//事务A未commit,不可以查询到事务B插入的值 | |
update test set name='ll' where id=3; | |
select * from test;//事务A未commit,但是由于where条件匹配了,就主动更新了快照,可以读取到更新后的值 |
从上可见,由于insert操作属于当前读,事务A在Bcommit前进行insert时,会被阻塞,但是select不会被阻塞
那么,在事务A中进行insert这个当前读操作就发生了幻读,因为这和select读取的结果不一致
2-1.2 Next-Key Locks
从MVCC的机制可知,其不能解决幻读,InnoDB引入了Next-Key Lock解决了此问题(关键在于上锁)
2-1.2.1 Record Locks
锁定一个记录上的索引,而不是记录本身
2-1.2.2 Gap Locks
锁定索引之间的间隙
假设select * from test where id between 10 and 20 for update;
那么id从10到20的数据就会被锁住,尽管10-20之间可能没有数据
2-1.2.3 Next-Key Locks
首先会对指定的行的索引上锁,且会对索引之前所在的gap上锁
-
假设在事务A中执行select * from test where id=3 for update;且假设当前表格中有id为1,5,10
那么,有如下情况,在RR隔离级别下,如果id为
-
主键
不会触发next key lock,只会锁住主键
-
唯一索引
不会触发next key lock,会锁柱该唯一索引及对应的主键
-
非唯一索引
触发next key lock,会锁住id为(1,5]的区间
假设存在id为3的多行记录,那么会锁住所有3记录对应的主键之间的gap,且会锁住(1,3]这个区间
-
不是索引列
那么会对(-∞,+∞)上锁
这样就使得涉及到此范围内的数据相关操作按照事务串行的方式执行,从而解决幻读问题,如果是在RC隔离级别下,则不会触发next key lock
-
-
假设在事务A中执行select * from test where id>6 for update;且假设当前表格中有id为1,5,10,那么大于6的部分会被上gap lock
2-1.3 Serializable解决幻读
MySQL下的这个隔离级别解决幻读的方式很悲观,其会对事务中每条sql涉及到的资源进行上锁,这样就可以阻塞其他事务对数据的操作,从而使得事务可以串行化的执行
可见,这种方式的效率是很低的
一般情况下,在RR隔离级别时,可以通过使用Next-Key Locks的方式对敏感数据上锁,即可保证一定区域内幻读的解决
触发Next-Key Locks有select for update和当前读操作
# 参考
数据库基础(四)Innodb MVCC实现原理 - 知乎 (zhihu.com)
MySQL :: MySQL 5.7 Reference Manual :: 14.7.1 InnoDB Locking
MySQL 加锁处理分析 - tutar - 博客园 (cnblogs.com)
2-2. 意向锁与锁
2-2.1 多粒度树
其定义如下
- 以树形结构表示多级别封锁粒度
- 根节点是整个数据库,下一级是关系也即数据库表,再下一级是元组,也即表记录
- 叶节点表示最小的封锁粒度
MySql中一般封锁的两个粒度为
-
表级锁
对关系上锁
-
行级锁
对元组上锁
2-2.2 意向锁
2-2.2.1 背景
假设事务A对表R上了一个行级锁,且是排他锁,此时事务B想要对表R上表级共享锁,那么需要进行如下检查
- R是否存在表级排他锁
- R是否存在行级排他锁
显然,这样的检查是非常耗时的,为了加快检查效率,提出意向锁
2-2.2.2 定义
针对一个多粒度树,如果对一个节点上意向锁,那么代表想要对其下层节点上排他/共享锁;反之,如果相对某节点上排他/共享锁,需要对其上层节点上意向锁
2-2.2.3 分类
-
意向共享锁 (
I
ntentedS
hared lock)如果对一个节点加IS锁,那么意欲对其子节点加共享锁
-
意向排他锁(
I
ntented eX
clusive lock)如果对一个节点加IX锁,那么意欲对其子节点加排他锁
-
共享意向排他锁(
S
haredI
ntented eX
clusive lock)先对其上S锁,再上IX锁
意向锁和普通锁的相容矩阵如下
S | X | IS | IX | SIX | |
---|---|---|---|---|---|
S | Y | N | Y | N | N |
X | N | N | N | N | N |
IS | Y | N | Y | Y | Y |
IX | N | N | Y | Y | N |
SIX | N | N | Y | N | N |
从上图中可以看出,IS和IX的笛卡尔积每一组都是相容的
2-2.3 MySQL中的意向锁
MySQL中为了加快加锁时检查现有锁的效率,会使用到意向锁,这样上锁逻辑如下
-
上行级X锁
需要对表上IX,之后对指定行上X,根据相容矩阵,后续的对于表级S/X锁和相同行的行级锁都不可以上
在没有意向锁的情况下,如果后续要上表级锁,需要对全表进行检查,判断是否存在X锁,在有表级锁IX的情况下,由于IX和表级锁S/X互斥,那么可以避免全表检查
-
上行级S锁
需要对表上IS锁,这样后续上表级锁X时,和IS不相容,就避免全表检查
可见,MySQL通过提供针对表的意向锁来解决了上锁时检查的开销,且意向锁不可以由用户操作
# 参考
详解 MySql InnoDB 中意向锁的作用 - 掘金 (juejin.cn)
数据库系统概论(高级篇)-中国人民大学-王珊、杜小勇、陈红等_哔哩哔哩_bilibili
2-3. InnoDB死锁
2-3.1 wait-for graph与wait for list
2-3.1.1 wait-for graph
innodb中,将每一个事务抽象成一个节点,那么假设存在t1,t2这两个事务,有如下上锁
t1 | t2 |
---|---|
begin; | |
begin; | |
select * from test where id=1 for update; | |
select * from test where id=2 for update; | |
select * from test where id=2 for update; | |
select * from test where id=1 for update; |
上面两个事务就构成了死锁,这时t1想要请求t2中拿到的id=2的行锁,t2想要请求t1拿到的id=1的行锁,所以构成如下wait-for graph
可见,出现了环路,也即存在死锁
2-3.1.2 wait for list
上面的两个事务t1和t2都会排入一个wait-for list中
2-3.2 事务的大小
innodb会根据事务中增、删、改的行数来确定一个事务的大小
2-3.3 InnoDB死锁应对
2-3.3.1 死锁检测(innodb_deadlock_detect
)
在默认情况下,innodb_table_locks会开启,在autocommit=0时,innodb会感知到lock table命令
死锁检测默认开启,在开启状态下,
- 会对wait-for graph进行检测,如果出现了环路,
- 会对wait-list进行检查,如果深度超过200,
则会选择最小的一个或几个事务进行回滚
2-3.3.2 超时回滚(innodb_lock_wait_timeout
)
此选项默认关闭,如果开启,那么当一个事务从尝试获取锁开始,会开始计时,当超出此阈值(默认50s),则会触发该事务的回滚
# 参考
MySQL锁(3):死锁_Jakob_Hu的博客-CSDN博客
MySQL :: MySQL 8.0 Reference Manual :: 15.7.5 Deadlocks in InnoDB