InnoDB中的锁

InnoDB中的锁

我自己总结,InnoDB锁的特点是多粒度共存、服务于特定的事务隔离级别。
此外,可以梳理出“锁”存在的目的:
1. 在Read Commit(提交读)级别上,需要解决的问题是脏读。
2. 在Repeatable Read(可重复读)级别上,需要解决的问题是脏读以及幻读

最后是在满足事务隔离级别的前提下,提高性能:
1. 利用MVCC多版本并发控制提高读取性能。
2. 特定情况下,升级、降级锁,例如在唯一主键的情况下,将下一键锁降为记录锁;或者在想要获取一个表中的行锁时,先利用意向锁这个逻辑上更高一级别的锁去检验是否能够获取。

InnoDB的常用锁如下:

共享锁和排他锁

MySql在行锁的实现上有两种级别:共享锁S和排他锁X。

  • 共享锁允许持有锁的事务读取该行的事务
  • 排他锁允许持有锁的事务更新或者删除该行

如果事务A对某行持有共享锁,那么事务B申请锁时,流程如下:

  • 事务B对该行申请的共享锁会很快被通过,A和B将同时持有该行的共享锁
  • 事务B如果申请排他锁,则无法立即得到权限。

如果A获取了某行的排他锁,那么事务B无论想要获取哪种锁,都必须等待A先释放。
共享锁可以分为行锁和表锁,是行为逻辑上的概念。

Intention Locks 意向锁

InnoDB支持多种粒度的锁定,也就是行锁和表锁可以共存。意向锁是支持这个特点的基石。

意向锁可以理解是表级别的锁,不是程序员手动设置的锁,而是InnoDB在替事务申请行锁、表锁过程中的衍生。意向锁有两种

  • 意图共享锁IS,事务打算设置针对表中的一些行设置共享锁时触发,语句SELECT ... FOR SHARE
  • 意图独占锁IX,事务打算在表中的某些行设置排他锁时触发,SELECT ... FOR UPDATE

此外,另外两种实际存在的表级别的锁是

  • 表级别的共享锁:LOCK table READ
  • 表级别的排他锁:LOCK table WRITE

意图锁的处理过程如下:

  • 在事务获取某些行的共享锁之前,事务应该先拥有这个表的意图共享锁IS
  • 在事务获取某些行的排他锁之前,事务应该先拥有这个表的意图独占锁IX

理解意图锁,首先确认下面的概念:

  1. 表锁的级别高于行锁
  2. InnoDB支持多粒度锁共存

假设事务A获得了表table1的行r的排他锁,此时事务B,想要申请table1的表级别共享锁,如果没有意图锁的概念,事务B获取表锁前需要做两件事:

  1. 查看表table1是否已经存在更高级别的表锁
  2. 查看表的每一行是否已经存在更高级别的行锁。(如果表共享锁与行独占锁同时存在,那么某一行可能被某一事务修改)

但是,如果有了意图锁,事务A在获得行r的排他锁的同时,还获得了表table1的意图共享锁,上述过程就变为:

  1. 查看表table1是否已经存在更高级别的表锁
  2. 查看表table1是否已经存在有冲突的意图锁
  3. 涉及具体行的时候,是否存在冲突的行锁

省去了第二步的遍历。我理解意图锁对程序员是透明的,是与行锁相伴的一种表锁,没有意图锁的话,多粒度锁的共存效率会降低。

几种表锁的关系如下

X IX S IS
X 冲突 冲突 冲突 冲突
IX 冲突 兼容 冲突 兼容
S 冲突 冲突 兼容 兼容
IS 冲突 兼容 兼容 兼容

Record Locks 记录锁

记录锁用于锁住被索引的记录,例如SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE,那么任务事务针对c1=10的记录的插入、更新、删除,都会被阻止。

即使涉及的表里没有建立索引,InnoDB将会创建一个隐藏的聚簇索引,然后用它来锁住记录。

Gap Lock间隙锁

InnoDB事务的默认隔离级别是RR,也就是可重复读,可重复读的含义是,同一个事务内,不会出现幻读,反复针对一条查询语句,读到的结果是相同的。可重复读的实现。依赖于间隙锁。间隙锁锁定一个查询范围,避免符合查询语句的范围内数据出现变更。

间隙锁可以是一个范围也可以是一个特定值,SELECT * FROM child WHERE id=100仅使用一个索引记录锁定id值为100的行。间隙锁的目的是抑制区域内的变更,因此,两个事务的间隙锁是可以重合的,本质上,目的都是一致的。

如果事务隔离级别是RC,也就是已提交读,那么gap lock将被禁用。
理解间隙锁,需要结合实际应用,这里暂且有这样的概念即可。

Next Key Lock

记录锁锁住的是一行数据,一条记录;间隙锁锁住的是一个类似(a, b)的开区间。next key lock是两者的结合,表示的是(a, b]这样一个左开右闭的区间,(a,b)使用间隙锁,b使用记录锁,记录锁。
对应的也有previous ket lock的概念,就是右开左闭的区间。
InnoDB在Repeatable Read的隔离级别下,使用该锁解决了幻读的问题。

Insert Intention Locks 插入意向锁

插入意向锁,插入意向锁是间隙锁的一种,无法获得插入意向锁时,插入操作将会被阻塞。
例如,事务ASELECT * FROM tables1 WHERE id>100 FOR UPDATE,那么表中id>100的部分会被加上间隙锁,此时,如果事务B想要INSERT INTO table1(id) VALUES (102),事务B在获取id=102的排他锁时,将尝试获得id=102的插入意向锁,显然,此时冲突,无法获取,事务B阻塞。可以用SHOW ENGINE INNODB STATUS查看到锁状态。

InnoDB事务加锁最佳实践

InnoDB事务中管理锁的过程遵循两阶段法则(two phase lock, 2PL),两阶段法则的意思是按照语句的执行顺序,加上必要的锁,等待语句执行之后,再按照加锁的逆顺序释放锁,根据两阶段锁的严格程度不同,又分为严格两阶段(strict 2pl)和强(strong strict 2pl)两阶段,两者的区别在于锁的释放时机。
strict two pahse
strong strict two phase lock
InnoDB中是S2PL的模式,只有回滚、commit的时候才释放锁,自然引出一个问题,
事务加锁的最佳实践
1. 按需加锁
2. 把最需要并发量的数据,放到最后加锁,
例如订单支付流程:

事务开始;
1. 获取用户余额写锁;
2. 扣除商品价格;
3. 获取商品数量的写锁;
4. 商品数量-1;
事务提交,锁释放;

这里3、4和1、2的顺序如果调换,那么事务的整个生命周期,商品数量的写锁会一直被占用,会大大降低订单的处理速度。而按照当前的顺序,每个事务只会占用该商品数据一般左右的时间,用户金额对于并发量的需求不大,因而放在次要位置,这样安排有助于提高业务处理的并发度。

最后

要说清楚各个情境下,间隙锁的表现,是很麻烦的事情,尤其考虑上聚簇索引、二级索引、无索引的情况后。
网上有一篇阿里何登成(资深技术专家 阿里巴巴数据库内核团队负责人)撰写的博文,比较详细,链接:http://hedengcheng.com/?p=771

发表评论