锁概述
undo log版本链 + Read View机制实现的MVCC多版本并发控制,可以防止事务并发读写同一数据时出现的脏读+不可重复读+幻读问题。但除脏读+不可重复读+幻读问题外,并发读写同一数据还有脏写问题。就是当多个事务并发更新同一条数据时,此时就可能会出现脏写问题,如下图示:
一.事务A对数据进行操作,将值修改为A;
二.事务B也对该数据进行操作,将值修改为B;
三.接着事务A进行了回滚,将值恢复成NULL;
四.事务B发现数据值B没有了,出现数据不一致;
脏写:一个事务修改了另一个没提交的事务修改过的值,导致数据可能不一致。因为这个没提交的事务有可能会回滚
锁分类
(1)从操作的粒度可分为表级锁、行级锁和页级锁
(2)从操作的类型可分为读锁和写锁
(3)从操作的性能可分为乐观锁和悲观锁
(1)从操作的粒度可分为表级锁、行级锁和页级锁
一.表级锁:每次操作锁住整张表
锁定粒度最大,发生锁冲突概率最高,并发度最低,应用在MyISAM、InnoDB、BDB存储引擎中。
二.行级锁:每次操作锁住一行数据
锁定粒度最小,发生锁冲突的概率最低,并发度最高,应用在InnoDB存储引擎中。
三.页级锁:每次锁定相邻的一组记录
锁定粒度界于表锁和行锁间,开销和加锁时间界于表锁和行锁间。并发度一般,应用在BDB存储引擎中。
(2)从操作的类型可分为读锁和写锁
一.读锁(S锁):共享锁,行级锁
针对同一份数据,多个读操作可以同时进行而不会互相影响。事务A对记录添加了S锁,可以对记录进行读操作,不能做修改。其他事务可以对该记录追加S锁,但是不能追加X锁。要追加X锁,需要等记录的S锁全部释放。
二.写锁(X锁):排它锁,行级锁
当前写操作没有完成前,它会阻断其他事务的写锁和读锁请求。事务A对记录添加了X锁,可以对记录进行读和修改,其他事务不能对记录进行读和修改。
三.IS锁:意向共享锁,表级锁
已加S锁的表肯定会有IS锁,反过来,有IS锁的表不一定会有S锁。
四.IX锁:意向排它锁,表级锁
已加X锁的表肯定会有IX锁,反过来,有IX锁的表不一定会有X锁。
(3)从操作的性能可分为乐观锁和悲观锁
一.乐观锁
一般的实现方式是对记录数据版本进行对比。在数据库表中有一个version字段,根据该字段进行冲突检测。如果有冲突就提示错误信息,所以乐观锁属于代码层面的锁控制。
二.悲观锁
修改一条数据时,为避免被其他事务同时修改,在修改前先锁定。共享锁和排它锁都是悲观锁的不同实现,属于数据库提供的锁机制。
根据加锁的范围,MySQL的锁可以分为:全局锁、表级锁和行级锁三类。
insert 插入流程
针对于事务当中的一笔 insert 操作,过程中的加锁步骤遵循下述流程:
• 1)申请插入意向锁: 本质上是去检查,插入位置所处范围是否存在间隙锁
• 2)唯一键冲突校验: 校验插入记录是否会和已存在记录发生唯一键冲突(Duplicate Key Conflict)
• 3)插入记录并加锁: 若没有唯一键冲突,则插入记录(草稿态),然后对其加行 X Lock
• 4)针对冲突记录加锁: 若发生唯一键冲突,则对引起冲突的行记录左右空隙加间隙锁,并申请该行记录的 S Lock
• 5)冲突记录双重校验:成功后,需要 double check 冲突记录的合法性,是的话返回唯
死锁案例
基于上述流程,下面分享一个因为 INSERT 操作而引发死锁的案例.
在操作开始前,数据表初始的数据状况如下表所示:
具体执行 SQL 如下表,在时刻 3 事务 B 插入 key = n 时因遭遇唯一键冲突,会对记录加左右间隙锁,并因为申请冲突记录的 S Lock 而陷入阻塞;而事务 A 在时刻 4 尝试插入 key = m 时则会因为事务 B 施加的间隙锁而陷入阻塞,最终形成死锁:
事务 A -> 等待事务 B 释放 key = n 的左右间隙锁
事务 B -> 等待事务 A 释放 key = n 的 X Lock
案例延伸探讨
针对上述死锁案例,我们额外展开探讨一个细节点:
案例中形成死锁的一个重要原因在于,事务 B 遭遇唯一键冲突后,选择先加间隙锁,再申请冲突记录的 S Lock,正是这样的加锁顺序才导致事务 A 后续的 INSERT 操作发生阻塞,最后引起锁资源的循环依赖.
试想一下,事务 B 在唯一键冲突后不申请间隙锁而是仅加冲突记录的行 S Lock(猜想一) ,亦或是把加锁顺序调整为先加行 S Lock,再加间隙锁(猜想二) ,这样是否就能够规避死锁问题呢?
针对猜想一,我们论证一下加间隙锁的必要性.
通过下述 SQL 我们演示一个不加间隙锁而导致的 badcase:
在时刻 3 事务 B 阻塞等待冲突记录行 S Lock,随后时刻 4 事务 A 回滚,因此事务 B 成功获得该行的 S Lock,并且发现行记录已经被删除,判断唯一键冲突问题已解;但与此同时,一个并发执行的事务 C 在此时见缝插针,又插入了一条唯一键相同的记录,这样就会导致事务 B 针对唯一键冲突的校验结果失准.
针对猜想二,其实存在的问题和猜想一是一样的,就是倘若事务 A 回滚,此时没有有效的手段拦截第三方并发事务的插入行为,因此事务 B 才需要先加间隙锁,阻止其他事务的并发插入行为,再进行后续的加行锁和校验操作.
基于 SQL 展示的反例如下: