文章目录
什么是锁?
数据库使用锁是为了支持对共享资源进行并发访问,提供数据的完成性和一致性。
lock与latch
在数据库中lock
与latch
都被称为锁
,两种是完全不同的2个概念:
latch
:闩锁(轻量级锁
)用来保证并发情况下线程操作临界资源的正确性,没有死锁检测机制。它又可以分为mutex
(互斥)和rwlock
(读写锁)。
lock
:对象是事务,用来锁的数据库中如表
、页
、行
。lock
对象一般在事物commint
或rollback
后释放,有死锁机制。
lock与latch对比
lock | latch | |
---|---|---|
对象 | 事务 | 线程 |
保护 | 数据库内容 | 内存数据结构 |
持续时间 | 整个事务过程 | 临界资源 |
模式 | 行锁、表锁、意向锁 | 读写锁、互斥量 |
死锁 | Waits-for graph、time out等机制进行死锁检测与处理 | 无死锁检测与处理(程序加锁顺序) |
存在于 | Lock Manager的哈希表中 | 每个数据结构对象中 |
lock与latch查看
latch
信息可以通过命令show engine innodb mutex;
查看。lock
信息可以通过命令show engine innodb status;
以及下列表观察锁信息:
SELECT * FROM information_schema.innodb_trx;
SELECT * FROM performance_schema.data_locks;
SELECT * FROM performance_schema.data_lock_waits;
锁的分类
按照加锁机制、兼容性、粒度、加锁方式将MySQL
锁分类如下图所示:
共享锁和排他锁
InnoDB
实现了标准的行级锁
,其中有两种类型的锁,共享锁(S
)和排他锁(X
)。
- 共享锁(
S
):允许持有该锁的事务读取一行数据。 - 排他锁(
X
):允许持有该锁的事务更新或删除一行数据。
如果事务T1
持有r
行上的一个共享锁(S
),那么来自不同事务T2
的请求对r
行上的一个锁的处理流程如下:
T2
获得S锁
。结果T1
和T2
都持有r
行上的S锁
。T2
无法获得X锁
。
如果事务T1
持有r
行上的排他锁(X
),则某个不同的事务T2
对r
行上的任何类型的锁的请求都不能获得。相反,事务T2
必须等待事务T1
释放它在r
行上的锁,这种情况称为锁不兼容。如果事务T1
获得r
行上的共享锁(S
),事务T2
立即获得r
行的共享锁(S
),这种情况称为锁兼容。
X | S | |
---|---|---|
X | 不兼容 | 不兼容 |
S | 不兼容 | 兼容 |
表锁
表锁是力度最大的锁,开销小、加锁快,不会出现死锁。但是由于粒度大,造成锁冲突的几率大,并发性能低。
表级别S、X锁
默认情况下InnoDB
存储引擎不会使用表级别的S
锁和X
锁,可以使用LOCK TABLES
命令来手动添加:
LOCK TABLES T READ
:对表T
加表级别S锁
。
LOCK TABLES T WRITE
:对表T
加表级别X锁
。
意向锁
InnoDB
支持多粒度锁,允许行锁和表锁共存。为了实现多粒度级别的锁,InnoDB
使用了意向锁。意向锁将锁的的对象分为多个层次,事务希望在更细粒度上进行加锁。数据库加锁的对象抽象成一棵树,从最下层依次往上排列为:行->页->表->数据库
,最底层的加锁粒度最细,对细粒度对象加锁首页要对粗粒度对象加锁,如下图所示:
如果要对表1
的r记录
上X锁
,那么需要对数据库
、表1
、页
上意向IX
,最后对r记录
上X锁
。其中任何一个部分导致等待,那么该操作需要等待粗粒度锁完成。例如在对r
记录加X锁
之前,已有事务对表1
进行了S
表锁,之后事务需要对r
记录所在表1
上加IX
,由于不兼容,所以该事务需要等待表锁操作完成释放。
意向锁是表级锁,它指事务稍后需要对表中的一行使用哪种类型的锁(共享的还是排他的)。有两种类型的意向锁:
意向共享锁(IS)
:表示事务打算在表中的单个行上设置共享锁。意向排他锁(IX)
:表示事务打算在表中的单个行上设置排他锁。
SELECT ... FOR SHARE
设置IS锁
, SELECT ... FOR UPDATE
设置IX锁
。
意向锁的规则如下:
- 在事务获得表中一行的
共享锁(S)
之前,它必须首先获得表上的IS锁
或更强的锁。 - 在事务获得表中一行的
排他锁(X)
之前,它必须首先获得表上的IX锁
。
表级锁类型兼容性如下(Y兼容,N不兼容):
X | IX | S | IS | |
---|---|---|---|---|
X | N | N | N | N |
IX | N | Y | N | Y |
S | N | N | Y | Y |
IS | N | Y | Y | Y |
如果一个锁与现有的锁兼容,那么它就会被授予请求事务。 但如果它与现有的锁冲突,则不会。事务会一直等待,直到冲突的现有锁被释放。 如果锁请求与现有的锁冲突,并且由于会导致死锁而不能授予锁,则会发生错误。
意向锁不会阻塞除全表请求以外(例如,LOCK TABLES…WRITE
)的任何东西。意向锁的主要目的是显示有人正在锁定表中的一行,或者将要锁定表中的一行。
意向锁的事务数据在SHOW ENGINE INNODB STATUS
和INNODB monitor
输出中显示类似如下:
TABLE LOCK table `test`.`t` trx id 10080 lock mode IX
自增锁
自增(AUTO-INC
)锁是一种特殊的表级锁,由插入到具有AUTO_INCREMENT
列的表中的事务获得。在最简单的情况下,如果一个事务正在向表中插入值,那么任何其他事务都必须等待对该表进行自己的插入,以便第一个事务插入的行接收连续的主键值。
对每个含有自增长列的表都有一个自增长计数器,当对表进行插入操作时计数器被初始化,执行如下语句获取计数器值:
SELECT MAX(auto_inc_col) FROM T FOR UPDATE;
这种锁特殊的表锁机制,为了提高插入性能,锁不是在一个事务完成后才释放,而是完成自增长值插入的SQL
语句后立即释放。
自增锁对于有AUTO_INCREMENT
列的并发插入性能较差,事务必须等到前一个插入完成,其次对于批量的插入另一个事务中的插入会被阻塞。 MySQL
针对此问题提供了innodb_autoinc_lock_mode
变量控制自增锁的算法。它允许选择如何在可预测的自动递增值序列和插入操作的最大并发性之间进行权衡。在分析innodb_autoinc_lock_mode
变量之前先对各种自增长的插入分类:
插入类型 | 说明 |
---|---|
简单插入(simple inserts ) | 能在插入前确定插入行数,INSERT...VALUES() 和 REPLACE 语句。不包含INSERT...ON DUPLICATE KEY UPDATE 这类语句。 |
批量插入(bulk inserts ) | 插入前不确定要插入的行数, INSERT ... SELECT , REPLACE ... SELECT 和 LOAD DATA 语句。 |
混合模式插入(mixed-mode inserts ) | 插入数据其中一部分自增长值是指定确定的,另外一部分是自增长的。例如:INSERT INTO t(c1,c2)values(1,'a'),(NULL,'b'),(NULL,'C'); 包含INSERT...ON DUPLICATE KEY UPDATE 这类语句。 |
innodb_autoinc_lock_mode
三种取值,如下:
innodb_autoinc_lock_mode值 | 说明 |
---|---|
0 | 所有类型的insert 语句都会获得表级AUTO-INC 锁。因为是表级锁,并发情况下AUOT-INC 锁的竞争导致并发性能差,不是首选项。 |
1 | MySQL 8.0 之前版本的默认值。"simple inserts "模式,会使用互斥量(mutex )对内存中的计数器进行累加操作。对于"bulk inserts "模式,使用传统表级别的AUTO—INC 锁。在这个模式下如果不考虑回滚操作,自增长列的值还是连续的,并且这个模式下statement-based replication 方式的主从复制数据一致性也能保证。如果已经使用AUTO-INC 锁方式产生自增长的值,然后还需再进行"simple inserts "操作,还是需要等待AUTO-INC 锁的释放。 |
2 | MySQL 8.0 版本默认值。这个模式下所有的INSERT 语句自增长值产生都是通过互斥量(mutex ),这是性能最高的方式。但是这样在并发插入的时候存在一定问题,导致自增长的值可能不是连续的。statement-based replication 方式的主从复制会出问题,因此使用这样模式任何时候都应该使用row-base replication 方式,这样才能保证最大的并发性能以及主从复制的一致性。 |
MDL锁
上面描述的表级别锁是针对表记录数据的锁。MySQL5.5
引入针对表结构的锁称为MDL(metadata lock)
锁,即元数据锁,元数据指的是描述数据的数据,在数据库中元数据,包括db
,table
,function
,procedure
,trigger
,event
等。
MDL
锁是为了保证并发环境下元数据和表数据的结构一致性(主要是保证DDL
操作与DML
操作之间的一致性)。如果事务T1
对表加了MDL
锁,那么T2
事务就不能对表结构进行变更,同样对于正在进行表结构变更时,不允许其它事务对表数据进行增删改查。
MDL锁有两种类型:
MDL读锁
:对一个表做增删该查操作时加MDL读锁。MDL写锁
:对一个表结构变更时加MDL写锁。
MDL锁引入解决的问题:
1.事务隔离:在可重复度隔离级别,SessionA在2次查询中,SessionB对比结构做了修改,两次结果查询就会不一致,无法满足可重复度要求。
2.数据复制:SessionA执行多条更新语句,SessionB对表结构做了变更并且提交,这样导致slave会先alter,再update出现复制错误现象。
行锁
行锁是粒度最小,行锁加锁开销性能大、加锁慢,并且会出现死锁,但行锁的锁冲突几率低,并发性能高。
InnoDB
存储引擎有3种行锁算法:
记录锁(Record Lock)
:单行记录上的锁间歇锁(Gap Lock)
:锁定一个范围,但不包含记录本身临界锁(Next-Key Lock)
:Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身
记录锁(Record Lock)
记录锁是索引记录上的锁。记录锁总是锁定索引记录,即使表没有定义索引。 对于这种情况,InnoDB
会创建一个隐藏的聚集索引,并使用这个索引来锁定记录。
例如:SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE;
防止任何其他事务插入、更新或删除t.c1值为10的行。
记录锁按照锁兼容分类:S型记录锁
和X型记录锁
。
-
当事务
T1
获取记录r
的S
型记录锁后,事务T2
也可以继续获取该记录的S
型记录锁,但是不可以获取X
型记录锁。 -
当事务
T1
获取记录r
的X
型记录锁后,事务T2
不可以继续获取该记录的S
型记录锁,更不可以获取X
型记录锁。命令
SHOW ENGINE INNODB STATUS
可以查看记录锁数据,记录锁的事务数据如下所示:
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 Lock)
间隙锁是索引记录之间间隙上的锁,或者是第一个索引记录之前或最后一个索引记录之后间隙上的锁。
例如: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
可以持有一个间隙上的共享间隙锁(Gap S-lock)
,而事务B
则持有同一个间隙上的独占间隙锁(Gap X-lock)
。允许冲突间隙锁的原因是,如果从索引中清除了一条记录,那么不同事务在记录上持有的间隙锁必须合并。
InnoDB
中的间隙锁是“纯粹的抑制性”的,这意味着它们的唯一目的是防止其他事务插入间隙
。间隙锁可以共存,一个事务获取的间隙锁并不阻止另一个事务获取同一间隙上的间隙锁。共享间隙锁和独占间隙锁没有区别。它们彼此不冲突,并且它们执行相同的功能。
间隙锁定可以显式禁用。如果将事务隔离级别更改为READ COMMITTED
,就会发生这种情况。在这种情况下,间隙锁定对于搜索和索引扫描是禁用的,只用于外键约束检查和重复键检查。
间隙锁的提出仅仅是为了防止插入幻影记录。
Next-Key锁
Next-Key
锁是索引记录上的记录锁和索引记录之前间隙上的间隙锁的组合。InnoDB
对于行的查询采用Next-Key锁
。
Next-KeyLock=Gap Lock+Record Lock
InnoDB
执行行级锁,当它搜索或扫描表索引时,它会在遇到的索引记录上设置共享或独占锁。因此,行级锁实际上是索引记录锁。索引记录上的Next-Key锁
也会影响该索引记录之前的“间隙”。也就是说,Next-Key锁
是一个索引记录锁加上索引记录之前的间隙上的间隙锁。如果一个会话在索引中的记录r
上有一个共享或独占锁,那么另一个会话就不能在索引顺序中紧靠r
之前的空隙中插入新的索引记录。
假设一个索引包含值10、11、13和20。这个索引可能的Next-Key锁
包括以下区间,其中圆括号表示排除区间端点,方括号表示包含端点:
(negative infinity, 10]
(10, 11]
(11, 13]
(13, 20]
(20, positive infinity)
如果事务T1使用Next-Key锁
,锁的了如下范围:(10, 11],(11, 13]。当插入插入12时,锁的范围变成(10, 11],(11, 12],(12, 13]。
当查询的索引包含唯一记录时,InnoD
B存储引擎会对Next-Key锁
进行优化,将其降级为记录锁(Record Lock)
,锁住索引本身而不是范围,从而提高并发性能。
/**会话A**/
select * from t where id=12 for update;
/**会话B**/
insert into t select 9;##无需等待,直接成功。
对于唯一索引的锁的,Next-Key锁降级为记录锁,仅存在于查询所有的唯一索引列。若唯一索引由多个列组成,而查询仅是查找多个唯一索引列种的一个,那么查询其实是range类型查询,而不是point累想查询,因此InnoDB存储引擎依然适用Next-Key进行锁定。
在默认事务隔离(REPEATABLE READ
)级别下,InnoDB
存储引擎使用Next-Key锁
来避免幻读
问题。
在SHOW ENGINE INNODB STATUS
,Next-Key
锁的事务数据类似如下:
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
操作在行插入之前设置。这个锁表示插入的意图,多个事务插入到相同的索引间隙中,如果它们没有插入到间隙中的相同位置,那么它们不需要等待对方。
假设有值为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
中,插入意图锁的事务数据如下所示:
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 ;;...
页锁
页锁的粒度是介于行锁和表锁之间的一种锁,BDB
存储引擎支持的一种锁机制。并发度一般,开销和加锁速度也介于行锁和表锁之间。
谓词锁
InnoDB
支持对包含空间数据(geometry
类型)的列进行空间索引,在多维数据中没有绝对排序的概念,所以Next-Key
锁不清楚哪个是“Next
”键。
为了支持具有SPATIAL索引
的表的隔离级别,InnoDB
使用了谓词锁。一个SPATIAL
索引包含最小边界矩形(MBR
)值,因此InnoDB
通过在查询使用的MBR
值上设置谓词锁来强制对索引进行一致的读取。其他事务不能插入或修改符合查询条件的行。
全局锁
全局锁是对整个数据库实例加锁,加锁后整个实例就处于只读状态,后续的数据增删改、DDL
以及更新事务提交语句将被阻塞。
使用场景是:做全库逻辑备份。
Flush tables with read lock
死锁
死锁是指两个或两个以上的事务在执行过程中,因争夺锁资源造成的一种相互等待的现象。
死锁示例:
时间 | SessionA | SessionB |
---|---|---|
1 | BEGIN; | |
2 | SELECT * FROM t where id=1 FOR UPDATE; | BEGIN; |
3 | SELECT * FROM t where id=2 FOR UPDATE; | |
4 | SELECT * FROM t where id=2 FOR UPDATE; | |
5 | SELECT * FROM t where id=1 FOR UPDATE;##产生死锁 |
事务A等待事务B释放id=2的行锁,而事务B等待事务A释放id=1的行锁,事务A和事务B互相等待对方释放资源进入死锁状态。出现时锁后有两种策略:
-
超时:当两个事务互相等待时,当其中一个等待时间超过设定的阀值时,进行回滚,另一个等待的事务继续进行。
InnoDB
中参数innodb_lock_wait_timeout
设置超时时间。超时机制简单,但是仅通过超时时间或者根据FIFO顺序选择魂滚对象,若超时事务占权重比较大,事务更新了很多行,占用较多的undolog,这是回滚不合适,因为这个事务相对另外一个事务占用的时间可能会更多。
-
死锁检测(
wait-for graph
):相比超时的方案,这个是一种更主动的死锁检测方式,InnoDB采用此方式。
wait-for graph
wait-for graph
要求数据库保存如下两种信息:锁的信息链表、事务等待链表。
根据上述2种链表可以构造出一张图,如果图中存在回路,就代表存在死锁。在wait-for graph
中,事务为图的节点,在图中事务T1->T2
边的定义为:
- 事务T1等待事务T2所占用资源
- 事务T1和T2之间在等待相同的资源,而事务T1发生在事务T2后面
如下示例:
图中事务等待链表所示总共4个事务t1、t2、t3、t4,在wait-for graph中有4个节点,t2对row1占S锁,t1对row占S锁。事务t1等待事务t2中的row1资源。在wait-for graph
图中t1->t2
的边。事务t2等待事务t1、t4中锁占用的row2对象,因此存在t2->t1
,t2->t4
的边。最终图如下所示:
如果所示(t1,t2)
存在回路,因此存在死锁。在每个事务请求锁并发生等待时都会判断是否存在回路,若存在则死锁,一般InnoDB
选择回滚undo
量最小的事务。
wait-for graph的死锁检测通常采用深度优先的算法实现。
乐观锁与悲观锁
无论是悲观锁还是乐观锁,都是人们定义出来的概念,可以认为是一种思想。
乐观锁
乐观锁总是认为不存在并发问题,每次去取数据的时候,总认为不会有其他线程对数据进行修改,因此不会上锁。但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用数据版本
或CAS
来实现。
乐观锁适用于多读
的应用类型,这样可以提高吞吐量,在Java
中java.util.concurrent.atomic
包下面的原子类使用了乐观锁的一种实现方式CAS
(Compare and Swap
)实现的。
数据版本
一般是在数据表中加上一个数据版本号version
字段,表示数据被修改的次数,当数据被修改时,version
值会加1
。当线程要更新数据值时,在读取数据的同时也会读取version
值,在提交更新时,若刚才读取到的version
值为当前数据库中的version
值相等时才更新,否则重试更新操作,直到更新成功。
update table set xxx=#{xxx}, version=version+1 where id=#{id} and version=#{version};
悲观锁
悲观锁认为对于同一个数据的并发操作,一定会发生修改的,哪怕没有修改,也会认为修改。因此对于同一份数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁并发操作一定会出问题。比如行锁、表锁、都是在操作前先上锁,当其它事务需要获取锁时阻塞。Java中Synchronized
关键字和Lock
的实现类都是悲观锁(先尝试CAS
乐观锁去获取锁,获取不到转为悲观锁)。悲观锁适合写操作多
的场景,先加锁可以保证写操作时数据正确。
显示锁与隐式锁
显示锁
通过特定语句加锁,例如:
SELECT * FROM T FOR SHARE;##显示共享锁
SELECT * FROM T FOR UPDATE;##显示排它锁
隐式锁
隐式锁:事务需要加锁的时,如果这个锁不可能发生冲突,InnoDB
会跳过加锁环节。
隐式锁是InnoDB
实现的一种延迟加锁机制,其特点是只有在可能发生冲突时才加锁,从而减少了锁的数量,提高了系统整体性能。
隐式锁是针对被修改的B+Tree记录,因此都是记录类型的锁。
隐式锁主要用于插入场景,在INSERT
语句执行过程中需要检测两种场景:
1.如果记录之间加有间隙锁,为了避免幻读此时不能插入记录。
2.如果INSERT
的记录与数据已有记录唯一键冲突,此时不能插入。
隐式锁在INSERT
过程中加锁,只有在特殊情况下隐式锁才会转为显示锁,这个转换动作不是加隐式锁的线程去做的,而是其它冲突线程做的。例如事务T1记录未提交,此时事务T2尝试对改记录加锁,那么T2必须先判断记录上保存的事务id是否活跃,如果活跃则帮助T1建立一个锁对象,而T2自身进入等待T1的状态。
隐式锁转换
根据索引的类型隐式锁转换分为两种类型:主键隐式锁转换
、二级索引隐式锁转换
。
主键隐式锁转换
InnoDB
每条记录聚簇索引有一个隐含的trx_id
字段,这个字段存储在聚簇索引B+Tree
中。trx_id
字段记录了最后改动记录事务id。如果当前事务插入一条记录后,该记录的trx_id
表示当前的事务id,如果其它的事务对该记录添加S
或X锁
时,首先比对该记录的trx_id
是否时当前活跃事务的id,如果是就帮助当前事务创建一个X锁,自己进入等待状态。
二级索引隐式锁转换
二级索引记录来说,本身并没有trx_id
隐藏列,但是在二级索引页面的 Page Header
部分有一个 PAGE_MAX_TRX_ID
属性,该属性代表对该页面做改动的最大的事务id ,如果PAGE_MAX_TRX_ID
属性值小于当前最小的活跃 事务id ,那么说明对该页面做修改的事务都已经提交了,否则就需要在页面中定位到对应的二级索引记录,然后回表找到它对应的聚簇索引记录,然后再重复“主键隐式锁转换
”流程。
锁内存结构
对记录加锁的本质是在内存中创建一个锁结构与之关联,InnoDB
在对不同的记录加锁时,如果如何如下条件,这些记录的锁放到同一个锁结构中。
1.加锁记录在同一个事务中
2.加锁记录在同一个页
3.加锁类型一致
4.等待状态一致
下图所示InnoDB存储引擎事务锁内存结构:
1.锁所在的事务信息:记录生存这个锁结构的事务信息,在内存结构中存储的是事务的指针,通过指针可以找到内存中关于该事务的更多信息。
2.索引信息:对于行锁需要记录加锁记录属于那个索引,存储的也是一个指针。
3.表锁/行锁信息:
- 表锁:记录是哪个表加的锁,以及其它一些信息。
- 行锁:记录三个重要信息:
Space ID
:记录所在表空间Page Number
:记录所在页号n_bits
:对于行锁一个记录对应一个bit位,一个页面中包含多个记录,用不同的bit
位来区分哪些记录加了锁。因此在行锁结构的末尾放置了一堆bit
位,这个n_bits
属性表示使用了多少bit
位。
4.type_mode:
一个32
位数,被分为lock_mode
、lock_type
、rec_lock_type
三部分。
-
锁的模式(lock_mode)
,占用低4位
,可选的值如下:lock_mode 说明 粒度 LOCK_IS
(十进制的 0)表示共享意向锁,也就是 IS
锁表锁 LOCK_IX
(十进制的 1)表示独占意向锁,也就是 IX
锁表锁 LOCK_S
(十进制的 2)表示共享锁,也就是 S
锁表锁、行锁 LOCK_X
(十进制的 3)表示独占锁,也就是 X
锁表锁、行锁 LOCK_AUTO_INC
(十进制的 4)表示 AUTO-INC
锁表锁 -
锁的类型( lock_type )
,占用第5~8
位,不过现阶段只有第5位
和第6位
被使用:LOCK_TABLE
(十进制的 16),也就是当第5个比特位置为1时,表示表级锁。LOCK_REC
(十进制的 32),也就是当第6个比特位置为1时,表示行级锁。 -
行锁的具体类型( rec_lock_type )
,使用其余的位来表示。只有在
lock_type
的值为LOCK_REC
时,也就是只有在该锁为行级锁时,才会被细分为更多的类型:
LOCK_ORDINARY
(十进制的 0 ):表示 next-key
锁 。
LOCK_GAP
(十进制的 512 ):也就是当第10个比特位置为1时,表示gap锁
。
LOCK_REC_NOT_GAP
(十进制的 1024):也就是当第11个比特位置为1时,表示记录锁 。
LOCK_INSERT_INTENTION
(十进制的 2048 ):也就是当第12个比特位置为1时,表示插入意向锁。
is_waiting
属性基于内存空间的节省,所以把is_waiting
属性放到了type_mode
这个32位的数字中:
LOCK_WAIT
(十进制的 256):当第9个比特位置为 1 时,表示is_waiting
为true
,也
就是当前事务尚未获取到锁,处在等待状态;当这个比特位为 0 时,表示 is_waiting
为 false
,也就是当. 前事务获取锁成功。
5.其他信息:
为了更好的管理系统运行过程中生成的各种锁结构而设计了各种哈希表和链表。
6.一堆比特位:
如果是行锁结构
的话,在该结构末尾还放置了一堆比特位,比特位的数量是由上边提到的n_bits
属性
表示的。InnoDB
数据页中的每条记录在 记录头信息
中都包含一个 heap_no
属性,伪记录 Infimum
的
heap_no
值为 0 , Supremum
的 heap_no
值为 1
,之后每插入一条记录, heap_no
值就增1
。 锁结
构 最后的一堆比特位就对应着一个页面中的记录,一个比特位映射一个 heap_no
,即一个比特位映射
到页内的一条记录。
锁监控
InnoDB
引擎可以查看状态变量inonda_row_lock
来分析锁的竞争情况。
mysql> show status like 'innodb_row_lock%';
+-------------------------------+-------+
| Variable_name | Value |
+-------------------------------+-------+
| Innodb_row_lock_current_waits | 0 |
| Innodb_row_lock_time | 72136 |
| Innodb_row_lock_time_avg | 18034 |
| Innodb_row_lock_time_max | 40826 |
| Innodb_row_lock_waits | 4 |
+-------------------------------+-------+
5 rows in set (0.35 sec)
状态量说明
变量 | 说明 |
---|---|
Innodb_row_lock_current_waits | 当前正在等待锁的数量 |
Innodb_row_lock_time | 从系统启动到现在锁定总时间长度(等待总时长) |
Innodb_row_lock_time_avg | 每次等待所花平均时间 |
Innodb_row_lock_time_avg | 从系统启动到现在等待最长的一次所花时间 |
Innodb_row_lock_waits | 系统启动后到现在总共等待次数 |
同时MySQL
把事务和锁信息记录在数据库表中,8.0
版本以下表可以查看对应信息:
SELECT * FROM information_schema.innodb_trx;
SELECT * FROM performance_schema.data_locks;## 事务锁信息,除了阻塞该事务的锁还可以查看该事务所持有的锁
SELECT * FROM performance_schema.data_lock_waits;
参考资料
1.MySQL 8.0 Reference Manual
2.Mysql技术内幕