InnoDB Locking

一、Shared and Exclusive Locks(共享锁和排他锁)

InnoDB实现标准行级锁,其中有两种类型的锁,共享锁(S)和排他锁(X)。

  • Shared Locks( S锁 ) 允许拥有该锁的事务去读行记录
  • Exclusive Locks( X锁 ) 允许拥有该锁的事务去修改或删除行记录

如果事务T1拥有行r的S锁,那么当另一个事务T2请求行r的锁时:

  • T2申请S锁,将被允许。这样,T1和T2都拥有行r的S锁
  • T2申请X锁,将不被允许。

如果事务T1拥有行r的X锁,那么当另一个事务T2请求行r的任何一种锁时,都不能立即允许,T2必须等待T1释放在行r上的锁。

二、Intention Locks(意向锁)

InnoDB支持多种粒度锁定,允许行锁和表锁并存。 诸如LOCK TABLES … WRITE之类的语句对指定表采用排他锁(X锁)。 为了使在多粒度级别上的锁定变得切实可行,InnoDB使用了意向锁。 意向锁是表级锁,用于指示事务稍后在表中的某行上需要哪种锁(共享锁或排他锁)。 有两种类型的意向锁:

  • intention shared lock (IS) 意向共享锁表明事务在一个表中的单个行上设置共享锁
  • intention exclusive lock (IX)意向排他锁表明事务事务在表中的单行上设置排它锁

例如,SELECT … LOCK IN SHARE MODE 设置IS锁,而 SELECT … FOR UPDATE 设置IX锁。

意向锁规则:

  • 在事务获取表中某行的共享锁时,必须先获取到表的IS锁或者更高级别的锁
  • 在事务获取表中某行的排他锁时,必须先获取到表的IX锁

表级锁类型的兼容性:
表级锁类型的兼容性

如果请求的锁与现有锁兼容,则将其授予事务,但如果与现有锁冲突,则不授予该锁。 事务等待直到冲突的现有锁被释放。 所请求的锁与持有的锁冲突是不可能被授予,因为这将会导致死锁,并且返回错误。

意向锁不会阻塞任何请求,除非将这个表锁住,例如,LOCK TABLE …. WRITE。意向锁的主要目的是显示某事务正在锁定一行,或者将锁定表中的一行数据。

意向锁的事务数据在SHOW ENGINE INNODB STATUS和InnoDB监视器输出中看起来类似于以下内容:

TABLE LOCK table test.t trx id 10080 lock mode IX

=================================================================================
拓展:
意向锁是有数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享 / 排他锁之前,InooDB 会先获取该数据行所在在数据表的对应意向锁。

在这里插入图片描述

users表:

 id	name
 1	ROADHOG
 2	Reinhardt
 3	Tracer
 4	Genji
 5	Hanzo
 6	Mccree
  1. 事务 A 先获取了某一行的排他锁,并未提交:
    SELECT * FROM users WHERE id = 6 FOR UPDATE;
  • 事务 A 获取了 users 表上的意向排他锁。
  • 事务 A 获取了 id 为 6 的数据行上的排他锁。
  1. 之后事务 B 想要获取 users 表的共享锁:
    LOCK TABLES users READ;
  • 事务 B 检测到事务 A 持有 users 表的意向排他锁。
  • 事务 B 对 users 表的加锁请求被阻塞(排斥)。
  1. 最后事务 C 也想获取 users 表中某一行的排他锁:
    SELECT * FROM users WHERE id = 5 FOR UPDATE;
  • 事务 C 申请 users 表的意向排他锁。
  • 事务 C 检测到事务 A 持有 users 表的意向排他锁。
  • 因为意向锁之间并不互斥,所以事务 C 获取到了 users 表的意向排他锁。
  • 因为id 为 5 的数据行上不存在任何排他锁,最终事务 C 成功获取到了该数据行上的排他锁。

总结:

  • InnoDB 支持多粒度锁,特定场景下,行级锁可以与表级锁共存。
  • 意向锁之间互不排斥,但除了 IS 与 S 兼容外,意向锁会与 共享锁 / 排他锁 互斥。
  • IX,IS是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,S发生冲突。
  • 意向锁在保证并发性的前提下,实现了行锁和表锁共存且满足事务隔离性的要求。

=================================================================================

三、Record Locks(记录锁)

记录锁是针对索引记录的锁。比如 SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE;阻止其他事务对t.c1=10行的插入、更新、删除操作。

记录锁总会锁定索引记录,即使一张表没有定义任何索引。比如,InnoDB创建隐式聚簇索引,并将该索引用于记录锁定。

记录锁的事务数据在SHOW ENGINE INNODB STATUS和InnoDB监视器输出中看起来类似于以下内容:

RECORD LOCKS space id 58 page no 3 n bits 72 index PRIMARY of table test.t
trx id 10078 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 4; hex 8000000a; asc ;;
1: len 6; hex 00000000274f; asc 'O;;
2: len 7; hex b60000019d0110; asc ;;

四、Gap Locks(间隙锁)

间隙锁定是对索引记录之间的范围锁定,或者对第一个索引之前或者最后一个索引之后的范围锁定。例如, SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE; 阻止其他事务插入t.c1=15的记录,不管是否已经存在这样的列,因为该范围的所有值是锁定的。

间隙可能跨越单个索引值,多个索引值,甚至为空。

间隙锁是性能和并发性之间权衡的一部分,并且在某些事务隔离级别中使用。

对于使用唯一索引锁定行来搜索特定行的语句,不需要间隙锁。 (这不包括查询条件中只包含多列中的某些列,唯一索引在这样的情况下,会使用间隙锁来锁定)例如,如果id列具有唯一索引,则以下语句仅使用 ID值为100的行的索引记录锁定,其他事务是否在前面间隙中插入行都没有关系:

SELECT * FROM child WHERE id = 100;

如果id列没有索引或者只是非唯一索引,间隙锁也会生效。

在这里还值得注意的是,可以通过不同的事务可以拥有冲突的间隙锁。例如,事务A可以在间隙上保留一个共享的间隙锁(间隙S锁),而事务B可以在同一间隙上保留排他的间隙锁(间隙X锁)。允许使用间隙锁冲突的原因是,如果从索引中清除记录,则必须合并拥有该记录的不同事务的间隙锁。

InnoDB中的间隙锁是“完全禁止的”,这意味着它们的唯一目的是防止其他事务插入。间隙锁可以共存。一个事务进行的间隙锁定不会阻止另一事务对相同的间隙进行间隙锁定。共享和排他间隙锁之间没有区别。它们彼此不冲突,并且执行相同的功能。

间隙锁定可以显式禁用。如果将事务隔离级别更改为READ COMMITTED或启用innodb_locks_unsafe_for_binlog系统变量(现已弃用),则会禁用间隙锁。在这种情况下,对于搜索和索引扫描,间隙锁将被禁用,并且仅用于外键约束检查和重复键检查。

使用READ COMMITTED隔离级别或启用innodb_locks_unsafe_for_binlog还有其他效果。 MySQL评估WHERE条件后,将释放不匹配行的记录锁。对于UPDATE语句,InnoDB进行“半一致”读取,以便将最新的提交版本返回给MySQL,以便MySQL可以确定该行是否与UPDATE的WHERE条件匹配。

五、Next-Key Locks(临键锁)

next-key lock = record lock + gap lock

InnoDB实现行级锁定的方式是,当它搜索或扫描表索引时,会在遇到的索引记录上设置共享或互斥锁。 因此,行级锁实际上是索引记录锁。 索引记录上的临键锁也会影响该索引记录之前的“区间”。 即,临键锁是索引记录锁定加上索引记录之前的区间上的间隙锁。 如果一个会话在索引中的记录R上具有共享或排他锁,则另一会话不能在索引顺序紧靠R之前的间隙中插入新的索引记录。

假定索引包含值10、11、13和20。此索引的临键锁定可能涵盖以下间隔,其中,圆括号表示排除区间端点,而方括号表示包括端点:

(negative infinity, 10]
(10, 11]
(11, 13]
(13, 20]
(20, positive infinity)

对于最后一个间隔,临键锁将区间锁定在索引中的最大值上方,并且“超级”伪记录的值高于索引中实际的任何值。 最高不是真正的索引记录,因此,实际上,此临键锁仅锁定最大索引值之后的区间。

默认情况下,InnoDB以REPEATABLE READ事务隔离级别运行。 在这种情况下,InnoDB使用next-key锁进行搜索和索引扫描,这可以防止幻读。

临键锁的事务数据在SHOW ENGINE INNODB STATUS和InnoDB监视器输出中看起来类似于以下内容:

RECORD LOCKS space id 58 page no 3 n bits 72 index PRIMARY of table test.t
trx id 10080 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 4; hex 8000000a; asc ;;
1: len 6; hex 00000000274f; asc 'O;;
2: len 7; hex b60000019d0110; asc ;;

===============================================================================
拓展:记录锁、间隙锁、临键锁

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

===============================================================================

六、Insert Intention Locks(插入意向锁)

插入意向锁是一种在插入之前通过INSERT操作设置的间隙锁。此锁发出插入意向信号,如果多个事务不是插入区间中的相同位置,则无需相互等待。假设存在索引记录,其值分别为4和7。单独的事务分别尝试插入值5和6,在获得插入行的排他锁之前,每个事务都使用插入意图锁来锁定4和7之间的间隙,但不互相阻塞,因为行是无冲突的。

下面的示例演示一个事务,在获取插入记录的排他锁之前,先进行了插入意向锁。该示例涉及两个客户端A和B。

客户端A创建一个包含两个索引记录(90和102)的表,然后启动一个事务,该事务将排他锁放置在ID大于100的索引记录上。排他锁在记录102之前包括一个间隙锁:

mysql> CREATE TABLE child (id int(11) NOT NULL, PRIMARY KEY(id)) ENGINE=InnoDB;
mysql> INSERT INTO child (id) values (90),(102);
mysql> START TRANSACTION;
mysql> SELECT * FROM child WHERE id > 100 FOR UPDATE;
±----+
| id |
±----+
| 102 |
±----+

客户端B开启一个事务在该区间内插入一行记录。事务在等待获得排他锁的同时获取插入意向锁。

mysql> START TRANSACTION;
mysql> INSERT INTO child (id) VALUES (101);

插入意向锁的事务数据在SHOW ENGINE INNODB STATUS和InnoDB监视器输出中看起来类似于以下内容:

RECORD LOCKS space id 31 page no 3 n bits 72 index PRIMARY of table test.child
trx id 8731 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 4; hex 80000066; asc f;;
1: len 6; hex 000000002215; asc " ;;
2: len 7; hex 9000000172011c; asc r ;;…

七、AUTO-INC Locks(自增锁)

AUTO-INC锁是一种特殊的表级锁,当插入到有AUTO_INCREMENT列的表中时事务需要获取自增锁。 在最简单的情况下,如果一个事务正在向表中插入值,那么任何其他事务都必须等待,以便第一个事务插入的行获取连续的主键值。

innodb_autoinc_lock_mode配置选项控制用于自增锁的算法。 它使您可以选择如何在可预测的自动增量值序列与插入操作的最大并发性之间进行权衡。

===================================================================================
拓展:
自增锁是一种特殊的表级锁,用以同一事务保持自增主键的连续性。自增锁一共有三种模式,由innodb_autoinc_lock_mode控制:

  • innodb_autoinc_lock_mode = 0 传统模式,所有的插入语句在开始的时候都需要先获取自增锁,语句结束之后才释放自增自增锁,最安全但并发性最差。
  • innodb_autoinc_lock_mode = 1 连续模式,InnoDB 中默认的方式,该模式对于可预测插入行数的插入进行了优化,一次可以批量生成连续的值。
  • innodb_autoinc_lock_mode = 2 交错模式,在这种锁定模式下,没有使用表级的自增锁,因此它的速度是最快的。但是该模式下并不能保证生成的值是连续,因此在主从复制或数据恢复的时候,主键可能与之前产生的不一致。

===================================================================================

八、Predicate Locks for Spatial Indexes(空间索引谓词锁)

InnoDB支持包含空间列的列空间索引。

为了处理涉及SPATIAL索引操作的锁定,临键锁定不能很好地支持REPEATABLE READ或SERIALIZABLE事务隔离级别。 多维数据中没有绝对排序概念,因此不清楚哪个是“下一个”键。

为了支持具有SPATIAL索引的表隔离级别,InnoDB使用谓词锁。 SPATIAL索引包含最小边界矩形(MBR)值,因此InnoDB通过在用于查询的MBR值上设置谓词锁定来强制对索引进行一致的读取。 其他事务不能插入或修改将匹配查询条件的行。

===================================================================================
拓展:
在多维空间数据中,没有绝对排序的概念,因此之前引入的间隙锁机制不能有效的处理空间数据的数据隔离。为此 InnoDB 中引入了空间索引谓词锁的机制,空间索引采用的是R-Tree 数据结构实现,空间索引包含了最小矩形边界的数据(MBR),因此 InnoDB 可以通过在 MBR 上加谓词锁来保证一致性读。

===================================================================================

拓展:
mysql锁分类

乐观锁

MySQL 中的乐观锁主要是应用程序级别自行实现的。主要思路为通过为每一行数据增加版本号的方式来处理。比如我们常常会设计这样的一张订单表:

CREATE TABLE `t_order` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `no` varchar(36) NOT NULL DEFAULT '',
  `status` tinyint(2) NOT NULL DEFAULT '0',
  `create_time` datetime NOT NULL,
  ...
  `version` int(11) NOT NULL DEFAULT '1'
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

订单表往往有状态,同时可能有多个线程在修改状态。此时如果不加以控制,状态可能会产生状态紊乱,因此我们常常会采取版本号的方式加以控制。在修改数据的时候同时需要比较上次数据的版本号,版本号不一致则不能够进行修改。

update t_order set status = 1,version=version+1 where id = 1 and version=#{lastVersion} 

悲观锁

我们常常说到的 MySQL 中的锁如表锁、行锁、页锁等等都是悲观锁。不同的引擎使用到的锁以及加锁机制不同,对于高并发下的数据库访问表现形式不同,因此我们对于其中采取的加锁原理需要有全面的了解。
在MySQL 中,按照粒度来划分共存在三种粒度的锁:表锁、行锁、页锁。其中BDB 引擎采用的是页锁, InnoDB 采用的是行锁,除此之外其余引擎采用的都是表锁。

MyISAM 中的锁
MySQL 中绝大部分引擎如MyISAM,Memory等采用的都是表锁的机制。由于 MyISAM 在使用上比较广泛一些,因此在此采用 MyISAM 来看看表锁。
表锁顾名思义是表级别上的锁,对于同一个表中的数据进行操作的时候,如果当前有一条数据在更新,那么其余更新请求不管是不是对于同一条数据更新的都需要等待锁释放后才能进行下一步操作。
从上面我们可以看出,表锁的级别还是比较重的,同一时刻只能有一个线程对同一表进行处理,其余线程需要干等着。为了提高新能 MySQL 中的表锁又分了 读锁写锁 两种类型。

  • 读锁,表级共享读锁,不会阻塞其他线程对于同一表的读请求,但会阻塞其他线程对同一表的写请求。
  • 写锁,表级独占写锁,会阻塞其他线程对于同一表的读写请求。

表锁兼容性如下:
表锁兼容性

加锁方式:
MyISAM,默认 SELECT 是会对涉及到的所有表加读锁,更新操作则会自动加写锁。也可以手动为查询或修改加读锁或写锁,加锁语句如下:

#加读锁
lock tables table_name read;
# 加写锁
lock tables table_name write;
#取消加锁
unlock tables;

BDB 中的锁
BDB 引擎中采用的页锁的机制,页锁指的是对于连续相邻的一组数据进行加锁因此锁的粒度介于表锁和行锁之间。

InnoDB 中的锁

InnoDB 是MySQL 中唯一实现了事务的引擎,出于提升数据库的并发度、提高数据库性能考虑,InnoDB 使用了行锁,即锁住某一条数据。InnoDB 中为了实现高并发以及数据隔离级别,引入了多种类型的锁,共有如下几种类型:记录锁意向锁区间锁临键锁插入意向锁自增锁空间索引谓词锁

记录锁(行级锁)
共享锁(S)与排他锁(X)是InnoDB 中标准行锁的实现机制。对于InnoDB 数据结构基于B+ Tree的聚集索引数据结构,因此共享锁与排他锁是加载索引上的。

  • 共享锁(S),允许事务读取一行数据。共享锁不会阻塞其他事务对于同一行的读请求,但会阻塞其他事务对同一行的写请求。
  • 排他锁(X),允许事务更新或删除一行数据,如果某一行数据被其他事务获取到了共享锁或排他锁,则需要等其他事务将该行锁全部释放后才能进行下一步更新操作。

意向锁(表级锁)
InnoDB 允许行锁与表级锁的共存。如果一个线程需要判断能否与获取某个表的锁,由于行锁的存在,除了要判断当前表是否有其他线程获取表锁外还需要对该表的所有数据进行一次遍历看是否能够获取锁。这样做的效率其实是很低的,为了弥补行锁与表锁之间的差异,InnoDB 引入了意向锁来处理,意向锁表明该事务后续想要申请某一行数据的共享锁或排他锁的意图。即:先需要向表申请意向锁,再申请某一行的行锁
意向锁分为意向共享锁(IS)意向排他锁(IX)

  • 意向共享锁(IS),表明一个事务意图获取一行数据的共享锁。
  • 意向排他锁(IX),表明一个事务意图获取一行数据的排他锁。

意向锁与行锁的兼容性:
意向锁与行锁兼容性

如果某一事务申请的锁与现有锁兼容,则可以获取到该锁,否则需要等到锁被释放才能被授予。手动加锁方式:

#加意向共享锁
select ... lock in share mode;
#加意向排他锁
select ... for update;

区间锁(行级锁)
区间锁指的是锁住两个索引之间区间的锁,作用在索引上不允许其他线程操作锁住区域。考虑如下情形,假设我们有一表test,表中有以下数据:

idname
1A
3C
7G

并发事务时序如下:
在这里插入图片描述

如若没有相关的机制,删除操作将把新插入的id=2的数据一并删除掉。区间锁正是为了解决这一问题而引入。我们知道 Read Committed 事务隔离级别下存在不可重复读的现象,在Read Repeatable 级别下,引入了间隙锁来解决这一问题。由于间隙锁锁住区域其余事务不能够进行操作,因此 InnoDB 的Read Repeatable其实是已经解决了幻读的现象。因此间隙锁是需要在Read Repeatable级别才有,加锁语句如下:

select id,name from test where id between 1 and 3 for update;

临键锁(行级锁)
临键锁是 Gap锁 + 记录锁的一种组合形式,作用于索引,加锁时会锁住索引及索引之间的间隙,是InnoDB 中的 Read Repeatable 默认加锁方式。以上面的test表为例,如若加 next-key 锁,可能产生的区间为:

(负无穷,1]
(1,3]
(3,7]
(7,正无穷)

插入意向锁(行级锁)
InnoDB 在执行行插入操作之前会对相应的索引区间加间隙锁。该锁表示了事务在该处的插入意图。如果多个事务没有在同一位置进行插入,那么它们是可以并发执行的毋需彼此等待。
以上述test表为例,
假设现在有两个事务,需要插入两条数据(4,D),(5,E):
在这里插入图片描述

如若按照Next-Key的方式进行加锁,由于事务1需要在(3,7]的区间进行插入,则需要对该区域进行加锁,因此事务1进行的时候事务2的插入需要等待事务1完成释放锁之后才可以进行插入,而使用插入意向锁由于两者虽都是在(3,7)这个区间内进行插入,但是插入的位置并不冲突,所以不会产生锁阻塞,这进一步提高了并发的性能。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值