进阶篇(15) 锁
事务的隔离性
由锁
来实现。
1. 概述
锁
是计算机协调多个迸程或线程并发访问某一资源
的机制。多线程并发访问数据时,需要保证数据在任何时刻只有一个线程在访问,保证数据的完整性
和一致性
。在开发过程中加锁是为了保证数据的一致性。
在数据库中,除传统的计算资源(如CPU、RAM、I/O等)的争用以外,数据也是一种共享资源。为保证数据的一致性,需要对并发操作进行控制
,因此产生了锁
。同时锁机制
也为实现MySQL的各个隔离级别提供了保证。锁冲突是影响数据库并发访问性能
的一个重要因素。
2. MySQL并发事务访问相同记录
2.1 读-读情况
并发事务读取相同的记录
。不会引起什么问题。
2.2 写-写情况
并发事务对相同的记录做出改动。
会发生脏写
,任何一种隔离级别都不允许。在多个事务对同一条记录做改动时排队执行
,排队的过程通过锁
实现。锁是内存中的结构
。一开始没有锁结构
和记录进行关联如图所示:
当事务想更改记录,先看内存中有没有与这条记录关联的锁结构
,没有则在内存中生成一个锁结构
与之关联。比如,事务 T1 要对这条记录做改动:
2.3 读-写或写-读情况
读-写
或写-读
,一个事务读取,另一个改动。这种情况下可能发生脏读
、不可重复读
、幻读
。
2.4 并发问题的解决方案
怎么解决脏读
、不可重复读
、幻读
这些问题呢?其实有两种可选的解决方案:
- 方案一:读操作利用多版本并发控制(
MVCC
,下章讲解),写操作进行加锁
。 - 方案二:读、写操作都采用
加锁
的方式。
有些业务不允许读取记录的旧版本。比如银行存款的事务,先读出余额,再加上本次存款数,最后写到数据库。读取余额后,不想让别的事务再访问该余额,本次事务完成才可以访问余额。
读取记录时需要进行加锁
,意味着读
操作和写
操作也像写-写
操作那样排队执行
。
脏读
:当前事务读取了另一个未提交事务写的一条记录。如果在写记录时给记录加锁,事务结束时再解锁,那么当前事务无法读取该记录,就不会有脏读问题。
不可重复读
:当前事务读取记录,另一个事务对该记录改动后提交,当前事务再次读取时会获得不同的值。当前事务读取记录时加锁,事务结束时再解锁,另一个事务就无法修改该记录,不会发生不可重复读。
幻读
:当前事务读取一个范围的记录,另外的事务向该范围插入新记录,当前事务再次读取该范围记录时发现新插入记录。加锁解决幻读问题有些麻烦,当前事务在第一次读取记录时幻影记录并不存在,所以读取的时候加锁就有点尴尬(不知道给谁加锁)
- 对比发现:
- 采用
MVCC
方式的话,读-写
操作彼此并不冲突,性能更高
。 - 采用
加锁
方式的话,读-写
操作彼此需要排队执行
,影响性能。
- 采用
一般情况下我们采用MVCC
来解决读-写
操作并发执行的问题,但是业务在某些特殊情况下,要求必须采用加锁
的方式执行。
3. 锁的不同角度分类
3.1 从数据操作的类型划分:读锁、写锁
读锁(共享锁)
:英文S
表示。针对同一份数据,多个事务的读操作可同时进行,相互不阻塞的。写锁(排他锁)
:英文X
表示。写操作没有完成前会阻断其他写锁和读锁。确保给定的时间里,只有一个事务能执行写入,并防止其他用户读取正在写入的资源。
对于InnoDB来说,读锁和写锁可以加在表上,也可以加在行上。
1. 锁定读
采用加锁
方式解决脏读
、不可重复读
、幻读
时,读取记录时需要获取该记录的S锁
,其实是不严谨的,有时需要在读取记录时获取X锁
,来禁止别的事务读写该记录,为此MySQL提出了两种比较特殊SELECT
语句格式:
- 对读取的记录加
S锁
:
SELECT ... LOCK IN SHARE MODE;
# 或
SELECT ... FOR SHARE; #(8.0新增语法)
事务执行该语句会为读取到的记录加S锁,这样允许别的事务继续获取这些记录的S锁,但不能获取X锁(比如使用SELECT ... FOR UPDATE语句来读取这些记录,或直接修改这些记录)。别的事务想获取这些记录的x锁,会阻塞到当前事务提交后将记录上的S锁释放。
- 对读取的记录加
X锁
:
SELECT ... FOR UPDATE;
事务执行该语句会为读取到的记录加X锁,不允许其他事务获取这些记录的S锁和X锁。其他事务获取锁时会阻塞到当前事务提交后将记录的X锁释放。
MySQL8.0新特性:
5.7及之前的版本,如果获取不到锁,会一直等待,直到innodb_lock_wait_timeout
超时。8.0版本中添加NOWAIT
、SKIP LOCKED
语法。
- NOWAIT:如果查询的行已经加锁,会立即报错返回
- SKIP LOCKED:如果查询的行已经加锁,只返回结果中不包含被锁定的行
2. 写操作
DELETE
:在B+
树中定位这条记录,获取该记录的X锁
,再执行delete mark
操作。UPDATE
:三种情况:- 1:未修改记录的主键,并且被更新的列占用的存储空间未发生变化。先在
B+
树定位记录,获取记录的X锁
,在原记录的位置进行修改。 - 2:未修改记录的主键,并且至少有一个被更新的列占用的存储空间发生变化。先在
B+
树中定位记录,获取记录的X锁
,将记录彻底删除,最后再插入一条新记录。 - 3:修改记录的主键,则相当于在原记录上做
DELECT
操作之后再来一次INSERT
操作。
- 1:未修改记录的主键,并且被更新的列占用的存储空间未发生变化。先在
INSERT
:一般情况插入记录的操作不加锁,通过隐式锁
保护记录在事务提交前不被别的事务访问。
3.2 从数据操作的粒度划分:表级锁、页级锁、行锁
为了提高数据库的并发度,每次锁定的数据范围越小越好,理论上每次只锁定当前操作的数据的方案会得到最大的并发度,但是管理锁是很耗资源
的事情(获取、检查、释放锁等动作)。因此DBMS需要平衡高并发响应
和系统性能
,这样就产生了锁粒度
的概念。
3.2.1 表锁
MySQL中最基本的锁策略,锁定整张表,不依赖于存储引擎
,开销最少
,可以很好的避免死锁
。粒度大带来最大的负面影响就是出现锁资源争用的概率也会最高,导致并发率大打折扣
。
① S锁、X锁
在对某个表执行CRUD语句时,InnoDB不会添加表级别的S锁
或者X锁
的。而诸如ALTER TABLE
这类的DDL
语句时,其他事务对该表并发执行CRUD语句会被表级X锁阻塞。同理,某个事务对表执行CRUD语句时,其他会话中对表执行DDL
语句也会发生阻塞。这个过程是通过在server层
使用元数据锁(下面讲)
实现。
查看表级锁:
SHOW OPEN TABLES; -- In_use 片段
UNLOCK TABLES; -- 解除所有表锁
一般情况不会使用InnoDB提供的表级S锁
和X锁
。只在一些特殊情况下,比方说崩溃恢复
过程中用到。比如,在系统变量autocommit=0,innodb_table_locks = 1
时,手动
获取InnoDB提供的表t 的S锁
或者X锁
可以这么写:
-
LOCK TABLES t READ
:对表t
加表级别的S锁
。 -
LOCK TABLES t WRITE
:对表t
加表级别的X锁
。
总结:MyISAM在执行查询语句前,会给涉及的所有表加读锁,在执行增删改操作前,会给涉及的表加写锁。InnoDB
存储引擎不会为这个表添加表级别的读锁
或者写锁
。
② 意向锁 (intention lock)
InnoDB 支持多粒度锁
,即允许行级锁
与表级锁
共存,而意向锁就是其中的一种表锁
。
1、意向锁的存在是为了协调行锁和表锁的关系,支持多粒度(表锁与行锁)的锁并存。
2、意向锁是一种不与行级锁冲突的表级锁
,这一点非常重要。
3、表明某个事务正在某些行持有了锁或该事务准备去持有锁
意向锁分为两种:
-
意向共享锁(IS):事务有意向对表中的某些行加共享锁
-- 事务要获取某些行的 S 锁,必须先获得表的 IS 锁。 SELECT column FROM table ... LOCK IN SHARE MODE;
-
意向排他锁(IX):事务有意向对表中的某些行加排他锁
-- 事务要获取某些行的 X 锁,必须先获得表的 IX 锁。 SELECT column FROM table ... FOR UPDATE;
意向锁由存储引擎自己维护
,用户无法手动操作。为数据行加共享/排他锁之前,InooDB先获取其所在数据表的对应意向锁
。
意向锁要解决的问题
给某行数据加排它锁,数据库会自动给更大一级的空间(数据页/数据表)加意向锁,告知该数据页/表有人给某行数据上过排它锁,当其他人想要获取数据页/表排它锁时,只需知道该表是否有意向排它锁即可。
- 事务想要获取表中记录的共享锁,需要在数据表上
添加意向共享锁
- 事务想要获取表中记录的排它锁,需要在数据表上
添加意向排他锁
③ 自增锁(AUTO-INC锁)
可以为表的某个列添加 AUTO_INCREMENT 属性。举例:
CREATE TABLE `teacher` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ;
由于id字段声明了AUTO_INCREMENT,意味着在书写插入语句时不需要为其赋值,SQL语句修改 如下所示
INSERT INTO `teacher` (name) VALUES ('zhangsan'), ('lisi');
上边的插入语句并没有为id列显式赋值,所以系统会自动为它赋上递增的值。这种插入数据的模式为简单插入
,所有插入数据的方式总共分为三类:
1. Simple inserts
(简单插入)
可以预先确定要插入的行数
的语句。上面举的例子就属于该类插入,已经确定要插入的行数。
2. Bulk inserts
(批量插入)
事先不知道要插入的行数
的语句。比如 INSERT ... SELECT
, REPLACE ... SELECT
和 LOAD DATA
语句。 InnoDB在每处理一行,为AUTO_INCREMENT列分配一个新值。
3. Mixed-mode inserts
(混合模式插入)
这些是“Simple inserts”语句但是指定部分新行的自动递增值。例如 INSERT INTO teacher (id,name) VALUES (1,'a'), (NULL,'b'), (5,'c'), (NULL,'d');
只指定了部分id的值。而NULL的两条记录ID一个为2,另一个为6
对于上面数据插入的案例,MySQL采用自增锁
实现,自增锁是当向使用含有AUTO_INCREMENT列的表中插入数据时需要获取的表级锁。执行插入语句时加自增锁,为每条待插入记录的自增列分配递增的值,在语句结束后把自增锁释放。事务持有自增锁时,其他事务的插入语句都要被阻塞,保证语句中分配的递增值连续。也正因为此,其并发性显然并不高
,所以innodb通过innodb_autoinc_lock_mode
的三种取值提供不同的锁定机制,来显著提高SQL语句的可伸缩性和性能:
0(“传统”锁定模式)
所有类型的insert语句都会获得自增锁,用于插入具有自增列的表。上面的例子就是该模式。1(“连续”锁定模式)
在 MySQL 8.0 之前,连续锁定模式是默认
的。bulk inserts
仍使用自增锁。对于Simple inserts
,则通过在mutex(轻量锁)
的控制下获得所需数量的自动递增值来避免自增锁, 它只在分配过程的持续时间内保持,而不是直到语句完成。如果其他事务保持自增锁,则Simple inserts
需要等待释放。2(“交错”锁定模式)
从 MySQL 8.0 开始,交错锁模式是默认
设置。 自动递增值保证
在所有并发执行的所有类型的insert语句中是唯一
且单调递增
的。但是由于多个语句可以同时生成数字,为任何给定语句插入的行生成的值可能不是连续的。
④ 元数据锁(MDL锁)
保证读写的正确性。比如,查询正在遍历表中的数据,而执行期间另一个线程对这个表结构做变更
,增加了一列,那么查询线程拿到的结果跟表结构对不上,肯定是不行的。
因此,当对一个表做增删改查操作的时候,加MDL读锁;当要对表做结构变更操作的时候,加MDL写锁。
读锁之间不互斥,因此可以有多个线程同时对一张表增删改查(只是元数据锁不互斥,具体情况还得看行X锁、表X锁之类的)。读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性,解决了DML和DDL操作之间的一致性问题。不需要显式使用
,在访问一个表的时候会被自动加上。
元数据锁演示
在一个会话中:
BEGIN;
SELECT * FROM xxx; --自动添加MDL锁
另一个会话中想为这个表添加一个字段会被阻塞:
ALTER TABLE xxx ADD c INT;
查看:
SHOW PROCESSLIST;
3.2.2 InnoDB中的行锁
行锁也称为记录锁,锁住某一行。MySQL服务器层没有实现行锁机制,行锁只在存储引擎层实现。
优点: 锁定力度小,锁冲突概率低,并发度高
缺点: 锁开销比较大
,加锁会比较慢,容易出现死锁
情况
InnoDB与MyISAM的最大不同有两点:一是支持事务;二是采用了行级锁。
① 记录锁(Record Locks)
把一条记录锁上,官方的类型名称为:LOCK_REC_NOT_GAP
。
记录锁是有S锁和X锁之分的,称之为S型记录锁
和X型记录锁
。
② 间隙锁(Gap Locks)
MySQL
在REPEATABLE READ
隔离级别下可以解决幻读,,可以使用MVCC
或加锁
解决。但加锁方案解决有个大问题,事务在第一次读取时幻影记录尚不存在,无法给这些幻影记录
加上记录锁
。InnoDB提出了一种称之为Gap Locks
的锁,官方的类型名称为:LOCK_GAP
。
id=8的记录加了gap锁,意味着不允许别的事务在id值为8的记录前边的间隙插入新记录
,即id列在(3, 8)区间的新记录不允许立即插入的。比如,另一个事务想插入id=4的新记录,它定位到该条新记录的下一条记录(id=8)上有gap锁,就会阻塞插入,直到拥有这个gap锁的事务提交之后才可以被插入。
gap锁仅仅是为了防止插入幻影记录而提出的。虽然有共享gap锁
和独占gap锁
这样的说法,但作用相同。而且如果对一条记录加了gap锁,并不限制其他事务对这条记录加记录锁或者继续加gap锁。
session 2并不会被堵住。因为表里并没有id=5这个记录,因此session 1加的是间隙锁(3,8)。而session 2也是在这个间隙加的间隙锁。它们有共同的目标:保护这个间隙,不允许插入值。
给记录加gap锁
只是不允许其他事务往这条记录前边的间隙插入新记录,那对于最后一条记录(id=20)的记录之后的间隙该咋办?这时我们在讲数据页时介绍的两条伪记录派上用场了:
Infimum
记录,表示该页面中最小的记录。Supremum
记录,表示该页面中最大的记录。
为了阻止其他事务插入id值在(20,+∞)这个区间的新记录,我们可以给索引中的最后一条记录,20的那条记录所在页面的Supremum记录加上一个gap锁。
③ 临键锁(Next-Key Locks)
既想锁住某条记录
,又想阻止
其他事务在该记录前边的间隙插入新记录
。官方的类型名称为:LOCK_ORDINARY
。Next-Key Locks是在innodb
、事务级别在可重复读
的情况下使用的数据库锁,innodb默认的锁。
begin;
select * from student where id <=8 and id > 3 for update;
④ 插入意向锁(Insert Intention Locks)
事务插入
记录时需要判断插入位置是否被别的事务加了gap锁
(next-key锁
也包含gap锁
),有则等待,直到拥有gap锁
的那个事务提交。但是InnoDB规定事务在等待的时候也需要在内存中生成一个锁结构,表明有事务想在某个间隙
中插入
新记录,但是现在在等待。官方的类型名称为:LOCK_INSERT_INTENTION
。插入意向锁是一种Gap锁
,不是意向锁,在insert操作时产生。
事实上插入意向锁并不会阻止别的事务继续获取该记录上任何类型的锁。
3.2.3 页锁
在页的粒度
上进行锁定,锁定的数据比行锁多。使用页锁时,会出现数据浪费的现象,但最多也就是一个页上的数据行。页锁开销和锁定粒度介于表锁和行锁之间,会出现死锁,并发度一般。
每个层级的锁数量有限制,因为锁占用内存空间,锁空间大小有限
。当某个层级的锁数量超过了这个层级的阈值时,就会进行锁升级
。锁升级就是用更大粒度的锁替代多个更小粒度的锁,好处是占用的锁空间降低了,但同时数据的并发度也下降了。
3.3 从对待锁的态度划分:乐观锁、悲观锁
乐观锁和悲观锁并不是锁,而是锁的设计思想
。
1. 悲观锁
悲观锁总是假设最坏的情况,每次拿数据时都认为别人会修改,所以每次拿数据都会上锁,这样别人想拿这个数据就会阻塞
直到它拿到锁。比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁,当其他线程想要访问数据时,都需要阻塞挂起。
注意:select ... for update 语句执行过程中所有扫描的行都会被锁上,因此在MySQL中用悲观锁必须确定使用了索引,而不是全表扫描,否则将会把整个表锁住。
2. 乐观锁
乐观锁认为对数据的并发操作不会总发生,不用每次都对数据上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,也就是不采用数据库自身的锁机制,而是通过程序来实现。
1. 乐观锁的版本号机制
在表中设计一个版本字段 version
,第一次读时获取version字段取值。然后对数据进行更新或删除操作时,会执行UPDATE ... SET version=version+1 WHERE version=version
。此时如果已经有事务对这条数据进行了更改,修改就不会成功。
2. 乐观锁的时间戳机制
版本号机制一样,也是在更新提交的时候,将当前数据的时间戳和更新之前取得的时间戳进行比较,如果两者一致则更新成功,否则就是版本冲突。
乐观锁就是程序员自己控制数据并发操作的权限,基本是通过给数据行增加一个戳(版本号或者时间戳),从而证明当前拿到的数据是否最新。
3. 两种锁的适用场景
-
乐观锁
适合读操作多
的场景,相对来说写的操作比较少。它的优点在于程序实现
,不存在死锁
问题,不过适用场景也会相对乐观,因为它阻止不了除了程序以外的数据库操作。 -
悲观锁
适合写操作多
的场景,因为写操作具有排它性
。采用悲观锁的方式,可以在数据库层面阻止其他事务对该数据的操作权限,防止读 - 写
和写 - 写
的冲突。
3.4 按加锁的方式划分:显式锁、隐式锁
1. 隐式锁
查询不到
SELECT * FROM performance_schema.data_lock_waits\G;
如果事务插入一条记录后另一个事务:
- 使用
SELECT...
获取该记录的S锁或X锁。如果允许这种情况,可能产生脏读
- 修改这条记录,即获取X锁,可能产生脏写
这时之前提过的事务id
起作用了,聚簇索引和二级索引分开看下:
-
情景一: 对于聚簇索引记录,有一个
trx_id
隐藏列存储最后改动该记录的事务id
。当前事务中新插入一条聚簇索引记录后,该记录的trx_id
隐藏列为当前事务的事务id
,其他事务此时想对该记录添加S锁
或X锁
时,先看该记录的trx_id
隐藏列代表的事务是否是当前的活跃事务,如果是的话,那么就帮助当前事务创建一个X锁
(为当前事务创建一个锁结构,is_waiting
属性是false
),然后自己进入等待状态(为自己也创建一个锁结构,is_waiting
属性是true
)。 -
情景二: 二级索引记录本并没有
trx_id
隐藏列,但是在二级索引页面的Page Header
部分有一个PAGE_MAX_TRX_ID
属性标记对该页面做改动的最大的事务id
,如果PAGE_MAX_TRX_ID
属性值小于当前最小的活跃事务id
,说明对该页面做修改的事务都已经提交了,否则需要在页面中定位到对应的二级索引记录,回表找到对应的聚簇索引记录,再重复情景一
的做法。
即:事务对新插入的记录不显示加锁,由于事务id
的存在,相当于加了隐式锁
。别的事务对这条记录加S锁
或X锁
时,由于隐式锁
的存在,会先帮助当前事务生成一个锁结构,然后自己再生成一个锁结构后进入等待状态。隐式锁是一种延迟加锁
的机制,从而来减少加锁的数量。
2. 显式锁
通过特定的语句进行加锁,即为显示锁。
3.5 全局锁
全局锁就是对整个数据库实例
加锁。需要让整个库处于只读状态
时可以使用这个命令。全局锁的典型使用场景
是:做全库逻辑备份
。
全局锁的命令:
Flush tables with read lock
3.6 死锁
两个或多个事务相互占用同一资源,并请求锁定对方占用的资源,从而导致恶性循环。
事务1在等待事务2释放id=2的行锁,而事务2在等待事务1释放id=1的行锁。
1. 如何处理死锁
方式1: 等待,直到超时(innodb_lock_wait_timeout
=50s)
当一个事务等待时间超过设置的阈值时,就将其回滚
,另外事务继续进行。
缺点:对于在线服务这个时间无法接受
将此值修改短一些,比如1s?不合适,容易误伤普通的锁等待。
方式2: 使用死锁检测进行死锁处理
innodb还提供了wait-for graph
算法主动进行死锁检测,每当加锁请求无法立即满足并进入等待时,wait-for graph算法都会被触发。
要求数据库保存锁的信息链表
和事务等待链表
基于这两个表可以绘制等待图
有向图存在环则有死锁
发现死锁后,回滚undo量最小的事务
,让其他事务得以继续执行。将参数 innodb_deadlock_detect
设置为 on
开启。
缺点:每个被阻塞的线程,都要判断是不是由于自己的加入导致了死锁,时间复杂度o(n)。如果100个并发线程同时更新同一行,意味着要检测100 * 100= 1万次。
如何解决?
- 方式1:关闭死锁检测,但可能出现大量超时,有损业务
- 方式2:控制并发访问数量,如在中间件中实现对相同行的更新、进入引擎前排队。
如何避免死锁?
- 合理设计索引,使SQL尽量通过索引定位更少的行,减少锁竞争
- 调整SQL执行顺序,避免update/delete长时间持有锁的SQL语句在事务的前部分
- 尽量将大事务拆成多个小事务
- 并发较高的系统中不要显式加锁
- 如果业务允许,降低隔离级别,比如将RR调整为RC,可以避免很多因为gap锁造成的死锁
4. 锁的内存结构
对记录加锁的本质就是在内存中创建一个锁结构
与之关联,若一个事务对多条记录加锁,要创建多个锁结构吗?
理论上创建多个锁结构没问题,但是如果一个事务要获取10000条记录的锁,生成10000个锁结构也太崩溃了!所以决定在对不同记录加锁时,如果符合下边这些条件的记录会放到一个锁结构中。
- 在同一个事务中进行加锁操作
- 被加锁的记录在同一个页面
- 加锁类型相同
- 等待状态相同
1. 锁所在的事务信息
:
表锁
和行锁
都是在事务执行过程中生成的,记录该事务的信息。
此字段只是一个指针,通过指针可以找到内存中该事务的更多信息。
2. 索引信息
:
对于行锁
来说,需要记录 加锁的记录 属于哪个索引。也是一个指针。
3. 表锁/行锁信息
:
- 表锁:记录是对哪个表加的锁,和一些其他信息。
- 行锁:
Space ID
:记录所在表空间。Page Number
:记录所在页号。n_bits
:一条记录对应一个比特位,用不同的比特位区分哪条记录加了锁。为此在行锁结构的末尾放置了一堆比特位,该属性代表使用了多少比特位。
n_bits的值一般比页面中记录条数多一些。主要是为了之后在页面中插入了新记录后也不至于重新分配锁结构
4. type_mode
:
32位,被分成了lock_mode
、lock_type
和rec_lock_type
三个部分:
- 锁的模式(
lock_mode
),占用低4位,可选的值如下:LOCK_IS
(十进制的0
):表示共享意向锁,也就是IS锁
。LOCK_IX
(十进制的1
):表示独占意向锁,也就是IX锁
。LOCK_S
(十进制的2
):表示共享锁,也就是S锁
。LOCK_X
(十进制的3
):表示独占锁,也就是X锁
。LOCK_AUTO_INC
(十进制的4
):表示AUTO-INC锁
。
在InnoDB中,0、1、4都是表级锁,2、3X既可以是表级锁,也可以是行级锁。
- 锁的类型(
lock_type
),占用第5~8位,现阶段只有第5位和第6位被使用:LOCK_TABLE
(十进制的16
),第5个比特位置为1时,表示表级锁。LOCK_REC
(十进制的32
),第6个比特位置为1时,表示行级锁。
- 行锁的具体类型(
rec_lock_type
),使用其余的位来表示。只有在该锁为行级锁时,才会被细分为更多的类型: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
属性呢LOCK_WAIT
(十进制的256
) :第9个比特位置为1
时表示is_waiting
为true
,即当前事务尚未获取到锁,处在等待状态;为0
时,表示false
,即当前事务获取锁成功。
5. 其他信息
:
为了更好的管理系统运行过程中生成的各种锁结构而设计了各种哈希表和链表。
6. 一堆比特位
:
行锁结构
在结构末尾还放置了一堆比特位,数量是由上边提到的n_bits
属性表示。InnoDB数据页中的每条记录在记录头信息
中都包含一个 heap_no 属性,伪记录Infimum
的heap_no
值为0
,Supremum
的heap_no
值为1
,之后每插入一条记录,heap_no
值就增1。一堆比特位对应着一个页面中的记录,一个比特位映射一个heap_no
,即映射到页内的一条记录。
5. 锁监控
show status like 'innodb_row_lock%';
- Innodb_row_lock_current_waits:当前正在等待锁定的数量;
Innodb_row_lock_time
:从系统启动到现在锁定总时间长度;(等待总时长)Innodb_row_lock_time_avg
:每次等待所花平均时间;(等待平均时长)- Innodb_row_lock_time_max:从系统启动到现在等待最常的一次所花的时间;
Innodb_row_lock_waits
:系统启动后到现在总共等待的次数;(等待总次数)
其他监控方法:
MySQL把事务和锁的信息记录在了information_schema
库中,涉及到的表:INNODB_TRX
、INNODB_LOCKS
和INNODB_LOCK_WAITS
。
MySQL5.7及之前
,可通过INNODB_LOCKS查看事务的锁情况,但只能看到阻塞事务的锁。
MySQL8.0删除了INNODB_LOCKS,添加了performance_schema.data_locks
,可以通过其查看事务的锁情况,和MySQL5.7及之前不同,data_locks可以看到阻塞该事务的锁和该事务所持有的锁。
同时,INNODB_LOCK_WAITS也被performance_schema.data_lock_waits
所代替。
转载自:https://juejin.cn/post/7176697902826258488