引言
生产环境中经常会遇到锁等待与死锁相关的问题,这类问题通常比较紧急,而且由于锁相关影响因素较多,因此分析难度较大。
本文从最简单的一类锁等待开始,即并发 update 导致锁等待。
介绍
如果相同的 update 同时执行会发生什么呢?
实际上会发生锁等待,生产环境中就遇到过这种案例,并发 update 导致锁等待。
死锁建立在锁等待的基础上,因此需要先理解锁等待的机制与分析思路。本文通过一个最简单的并发 update 介绍锁等待的分析方法。
模拟
首先,声明事务隔离级别为 RR(REPEATABLE-READ)。
流程
两个 session 分别在开启事务的前提下执行相同的 update 语句导致锁等待。
session A | session B |
---|---|
begin; update t2 set name=’d’ where id=1; |
|
begin; update t2 set name=’d’ where id=1; |
|
ERROR 1205 (HY000): Lock wait timeout exceeded |
其中超时时间由系统参数 innodb_lock_wait_timeout 控制,默认值 50s,当前值 120s。
mysql> select @@innodb_lock_wait_timeout;
+----------------------------+
| @@innodb_lock_wait_timeout |
+----------------------------+
| 120 |
+----------------------------+
1 row in set (0.00 sec)
根据官方文档,innodb_lock_wait_timeout 参数控制 InnoDB 存储引擎中事务的行锁等待时间,超时回滚。
innodb_lock_wait_timeout
The length of time in seconds an InnoDB transaction waits for a row lock before giving up.
MySQL 5.7 中查看事务加锁的情况有两种方式:
-
使用 information_schema 数据库中的表获取锁信息; -
使用 SHOW ENGINE INNODB STATUS 获取锁信息。
下面分别使用这两种方式分析当前事务加锁的情况。
innodb_trx
information_schema.innodb_trx 表中存储了 InnoDB 存储引擎当前正在执行的事务信息。
其中:
-
TRX_TABLES_LOCKED 字段表示事务当前执行 SQL 持有行锁涉及到的表的数量,注意不包括表锁,因此尽管部分行被锁定,但通常不影响其他事务的读写操作;
TRX_TABLES_LOCKED
The number of
InnoDB
tables that the current SQL statement has row locks on. (Because these are row locks, not table locks, the tables can usually still be read from and written to by multiple transactions, despite some rows being locked.)
-
TRX_ROWS_LOCKED 字段表示被事务锁定的行数,其中可能包括被标记为删除但实际上未物理删除的数据行。
TRX_ROWS_LOCKED
The approximate number or rows locked by this transaction. The value might include delete-marked rows that are physically present but not visible to the transaction.
结果表明当前有两个未提交事务,不同点是其中一个执行中,一个锁等待,相同点是都在内存中创建了两个锁结构,而且其中一个是行锁。
mysql> select * from information_schema.innodb_trxG
*************************** 1. row ***************************
trx_id: 11309021
trx_state: LOCK WAIT
trx_started: 2022-11-22 17:40:16
trx_requested_lock_id: 11309021:190:3:2
trx_wait_started: 2022-11-22 17:42:25
trx_weight: 2
trx_mysql_thread_id: 1135
trx_query: update t2 set name='d' where id=1
trx_operation_state: starting index read
trx_tables_in_use: 1
trx_tables_locked: 1 # 1个表上有行锁
trx_lock_structs: 2 # 内存中2个锁结构
trx_lock_memory_bytes: 1136
trx_rows_locked: 1 # 1行数据被锁定
trx_rows_modified: 0
trx_concurrency_tickets: 0
trx_isolation_level: REPEATABLE READ
trx_unique_checks: 1
trx_foreign_key_checks: 1
trx_last_foreign_key_error: NULL
trx_adaptive_hash_latched: 0
trx_adaptive_hash_timeout: 0
trx_is_read_only: 0
trx_autocommit_non_locking: 0
*************************** 2. row ***************************
trx_id: 11309020
trx_state: RUNNING
trx_started: 2022-11-22 17:40:09
trx_requested_lock_id: NULL
trx_wait_started: NULL
trx_weight: 3
trx_mysql_thread_id: 1134
trx_query: NULL
trx_operation_state: NULL
trx_tables_in_use: 0
trx_tables_locked: 1 # 1个表上有行锁
trx_lock_structs: 2 # 内存中2个锁结构
trx_lock_memory_bytes: 1136
trx_rows_locked: 1 # 1行数据被锁定
trx_rows_modified: 1
trx_concurrency_tickets: 0
trx_isolation_level: REPEATABLE READ
trx_unique_checks: 1
trx_foreign_key_checks: 1
trx_last_foreign_key_error: NULL
trx_adaptive_hash_latched: 0
trx_adaptive_hash_timeout: 0
trx_is_read_only: 0
trx_autocommit_non_locking: 0
2 rows in set (0.00 sec)
从中可以看到与锁相关的事务,但是无法看到锁的具体类型。
innodb_locks
information_schema.innodb_locks 表中主要包括以下两方面的锁信息:
-
如果一个事务想要获取某个锁但未获取到,则记录该锁信息,即等锁事务; -
如果一个事务获取到了某个锁,但是这个锁阻塞了其他事务,则记录该锁信息,即持锁事务。
The INNODB_LOCKS table provides information about each lock that an InnoDB transaction has requested but not yet acquired, and each lock that a transaction holds that is blocking another transaction.
注意只有当事务因为获取不到锁而被阻塞即发生锁等待时 innodb_locks 表中才会有记录,因此当只有一个事务时,无法查看该事务所加的锁信息。
如下所示,锁超时之后查询 innodb_locks 表,结果为空。
mysql> select * from information_schema.innodb_locksG
Empty set, 1 warning (0.00 sec)
如下所示,锁超时之前查询 innodb_locks 表,结果表明所有事务共请求了两次 t2 表的主键索引值为 1 的记录上的 X 型行锁。
mysql> select * from information_schema.innodb_locks G
*************************** 1. row ***************************
lock_id: 11309021:190:3:2
lock_trx_id: 11309021
lock_mode: X # 排它锁
lock_type: RECORD # 行锁
lock_table: `test_zk`.`t2` # 表名
lock_index: PRIMARY # 主键索引
lock_space: 190
lock_page: 3
lock_rec: 2
lock_data: 1 # 主键值为1
*************************** 2. row ***************************
lock_id: 11309020:190:3:2
lock_trx_id: 11309020
lock_mode: X # 排它锁
lock_type: RECORD # 行锁
lock_table: `test_zk`.`t2` # 表名
lock_index: PRIMARY # 主键索引
lock_space: 190
lock_page: 3
lock_rec: 2
lock_data: 1 # 主键值为1
2 rows in set, 1 warning (0.00 sec)
从中可以看到具体请求的锁的类型,但是无法区分等锁事务与持锁事务。
innodb_lock_waits
information_schema.innodb_lock_waits 表中记录每个阻塞的事务是因为获取不到哪个事务持有的锁而阻塞。
结果表明 11309020 事务阻塞了 11309021 事务。
mysql> select * from information_schema.innodb_lock_waits;
+-------------------+-------------------+-----------------+------------------+
| requesting_trx_id | requested_lock_id | blocking_trx_id | blocking_lock_id |
+-------------------+-------------------+-----------------+------------------+
| 11309021 | 11309021:190:3:2 | 11309020 | 11309020:190:3:2 |
+-------------------+-------------------+-----------------+------------------+
1 row in set, 1 warning (0.00 sec)
从中可以看到事务之间锁的依赖关系,但是无法查看到持锁 SQL,因此通常需要将该表与其他表做关联查询。
关联查询
如下所示,可以在发生锁等待的现场关联查询 information_schema 数据库中的多张表表分析持锁与等锁的事务与 SQL。
mysql> SELECT r.trx_id waiting_trx_id,
-> r.trx_mysql_thread_id waiting_thread,
-> r.trx_query waiting_query,
-> b.trx_id blocking_trx_id,
-> b.trx_mysql_thread_id blocking_thread,
-> b.trx_query blocking_query
-> FROM information_schema.innodb_lock_waits w
-> INNER JOIN information_schema.innodb_trx b ON
-> b.trx_id = w.blocking_trx_id
-> INNER JOIN information_schema.innodb_trx r ON
-> r.trx_id = w.requesting_trx_id;
*************************** 1. row ***************************
waiting_trx_id: 11309021
waiting_thread: 1135
waiting_query: update t2 set name='d' where id=1
blocking_trx_id: 11309020
blocking_thread: 1134
blocking_query: NULL
1 row in set, 1 warning (0.00 sec)
注意其中从 information_schema.innodb_trx 表中查询到的 blocking_query 即持锁的 SQL 为空。
实际上,可以从 performance_schema.events_statements_current 表中查询到持锁 SQL。
mysql> select
-> wt.thread_id waiting_thread_id,
-> wt.processlist_id waiting_processlist_id,
-> wt.processlist_time waiting_time,
-> wt.processlist_info waiting_query,
-> bt.thread_id blocking_thread_id,
-> bt.processlist_id blocking_processlist_id,
-> bt.processlist_time blocking_time,
-> c.sql_text blocking_query,
-> concat('kill ',bt.processlist_id, ';') sql_kill_blocking_connection
-> from information_schema.innodb_lock_waits l join information_schema.innodb_trx b
-> on b.trx_id = l.blocking_trx_id
-> join information_schema.innodb_trx w
-> on w.trx_id = l.requesting_trx_id
-> join performance_schema.threads wt
-> on w.trx_mysql_thread_id=wt.processlist_id
-> join performance_schema.threads bt
-> on b.trx_mysql_thread_id=bt.processlist_id
-> join performance_schema.events_statements_current c
-> on bt.thread_id=c.thread_id G
*************************** 1. row ***************************
waiting_thread_id: 1178
waiting_processlist_id: 1135
waiting_time: 61
waiting_query: update t2 set name='d' where id=1
blocking_thread_id: 1177
blocking_processlist_id: 1134
blocking_time: 76
blocking_query: update t2 set name='d' where id=1
sql_kill_blocking_connection: kill 1134;
1 row in set, 1 warning (0.00 sec)
INNODB STATUS
SHOW ENGINE INNODB STATUS 命令用于查询 InnoDB 存储引擎标准监控的状态信息。
SHOW ENGINE INNODB STATUS displays extensive information from the standard InnoDB Monitor about the state of the InnoDB storage engine.
其中 TRANSACTIONS 部分的信息可用于分析锁等待与死锁。
TRANSACTIONS
If this section reports lock waits, your applications might have lock contention. The output can also help to trace the reasons for transaction deadlocks.
结果如下所示,TRANSACTIONS 部分包括两个未提交事务。
mysql> show engine innodb status G
*************************** 1. row ***************************
Type: InnoDB
Name:
Status:
=====================================
2022-11-22 17:42:50 0x7ff4df900700 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 50 seconds
...
------------
TRANSACTIONS
------------
# 下一个待分配的事务id信息
Trx id counter 11309022
# 清除旧MVCC行时使用的事务ID,该事务与当前事务之间的老版本数据未被清除
Purge done for trx's n:o < 11309020 undo n:o < 0 state: running but idle
# 每个回滚段都有一个History链表,这些链表的总长度等于64
History list length 64
# 各个事务的具体信息
LIST OF TRANSACTIONS FOR EACH SESSION:
# not started 空闲事务,表示事务已经提交并且没有再发起影响事务的语句
---TRANSACTION 422165848318464, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 422165848316640, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
# 事务ID等于11309021的事务,处于活跃状态154秒,正在使用索引读取数据行
---TRANSACTION 11309021, ACTIVE 154 sec starting index read
# 事务11309021正在使用1张表,有1张表有锁
mysql tables in use 1, locked 1
# 等锁,锁链表长度为2,占用内存1136字节,其中1把行锁
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 1135, OS thread handle 140689506727680, query id 13803596 127.0.0.1 admin updating
# 事务运行中SQL语句
update t2 set name='d' where id=1
# 锁等待发生时在等待的锁信息,已等待25秒
------- TRX HAS BEEN WAITING 25 SEC FOR THIS LOCK TO BE GRANTED:
# 等锁,在等待主键索引(index PRIMARY)上的行级别X锁(RECORD LOCK),没有间隙锁
RECORD LOCKS space id 190 page no 3 n bits 80 index PRIMARY of table `test_zk`.`t2` trx id 11309021 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
# 内存地址,用于调试
0: len 4; hex 80000001; asc ;; # 聚簇索引的值,80000001 表示主键值为1
1: len 6; hex 000000ac8fdc; asc ;; # 事务ID,对应十进制 11309020
2: len 7; hex 730000002a0b0d; asc s * ;; # unod记录
3: len 1; hex 64; asc d;; # 非主键字段的值,'d'
------------------
# 持锁,事务ID等于11309021的事务对t2表加了表级别的意向排它锁
TABLE LOCK table `test_zk`.`t2` trx id 11309021 lock mode IX
# 等锁,在等待主键索引(index PRIMARY)上的行级别X锁(RECORD LOCK),没有间隙锁
RECORD LOCKS space id 190 page no 3 n bits 80 index PRIMARY of table `test_zk`.`t2` trx id 11309021 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
0: len 4; hex 80000001; asc ;;
1: len 6; hex 000000ac8fdc; asc ;;
2: len 7; hex 730000002a0b0d; asc s * ;;
3: len 1; hex 64; asc d;;
# 事务ID等于11309020的事务,处于活跃状态161秒
---TRANSACTION 11309020, ACTIVE 161 sec
# 该事务有2个锁结构,其中1个行锁
2 lock struct(s), heap size 1136, 1 row lock(s), undo log entries 1
MySQL thread id 1134, OS thread handle 140689373869824, query id 13803593 127.0.0.1 admin
# 持锁,事务ID等于11309020的事务对t2表加了表级别的意向排它锁,IX锁之间兼容
TABLE LOCK table `test_zk`.`t2` trx id 11309020 lock mode IX
# 持锁,主键索引(index PRIMARY)上的行级别X锁(RECORD LOCK),没有间隙锁
RECORD LOCKS space id 190 page no 3 n bits 80 index PRIMARY of table `test_zk`.`t2` trx id 11309020 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
0: len 4; hex 80000001; asc ;; # 80000001 表示主键值为1
1: len 6; hex 000000ac8fdc; asc ;;
2: len 7; hex 730000002a0b0d; asc s * ;;
3: len 1; hex 64; asc d;;
...
----------------------------
END OF INNODB MONITOR OUTPUT
============================
从中可以看到事务持锁与等锁的详细信息,但是无法看到持锁的 SQL。
由于信息不全,因此 SHOW ENGINE INNODB STATUS 更适合分析死锁,因为死锁已经没有了现场,而锁等待通常现场还在,可以直接查看 information_schema 数据库中的表。
主要信息如下所示。
-
11309021 事务持有 t2 表的表级别意向排它锁,等待主键索引上的行级别 X 锁(RECORD LOCK),没有间隙锁;
---TRANSACTION 11309021, ACTIVE 154 sec starting index read
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
update t2 set name='d' where id=1
TABLE LOCK table `test_zk`.`t2` trx id 11309021 lock mode IX
RECORD LOCKS space id 190 page no 3 n bits 80 index PRIMARY of table `test_zk`.`t2` trx id 11309021 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
-
11309020 事务分别持有 t2 表的表级别意向排它锁与主键索引上的行级别 X 锁(RECORD LOCK),没有间隙锁。
---TRANSACTION 11309020, ACTIVE 161 sec
2 lock struct(s), heap size 1136, 1 row lock(s), undo log entries 1
TABLE LOCK table `test_zk`.`t2` trx id 11309020 lock mode IX
RECORD LOCKS space id 190 page no 3 n bits 80 index PRIMARY of table `test_zk`.`t2` trx id 11309020 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
因此,锁等待分析的结论如下所示:
-
update 操作需要获取两把锁,包括表级别的意向排它锁与行级别 X 锁(RECORD LOCK); -
并发 update 时由于意向锁之间兼容,而行级 X 锁之间冲突,导致发生锁等待。
原理
锁
首先为什么需要锁?
锁本质上是一种并发控制手段,用于解决事务在并发执行时可能引发的一致性问题。
并发事务访问相同数据基本上可以分为以下三种情况:
-
读-读,相互不影响,因此允许; -
写-写,会导致脏写,因此不允许,通过给记录加锁实现; -
读-写或写-读,会导致脏读、不可重复读、幻读。解决方案主要分两种: -
MVCC 多版本并发控制,保存符合条件的记录的多个版本,写操作针对最新版本,读操作针对历史版本。因此读-写不冲突; -
读写操作均加锁,每次都需要读取最新版本数据,读写操作均采用加锁方式,因此读-写冲突。
而 InnoDB 存储引擎支持事务与行锁,并实现了基于 MVCC 的事务并发处理机制。
锁的类型
如下所示,根据不同的维度,可以将锁分为不同的类型。
其中:
-
根据加锁机制,实际上就是锁的实现方式,可以将锁分为以下两类:
-
乐观锁,先加锁后访问,传统的关系型数据库使用这种锁机制; -
悲观锁,先访问后加锁,常见实现如 CAS、版本号控制。 -
根据兼容性,可以将锁分为以下两类:
-
共享锁,Shared-Lock,S 锁,读锁; -
排它锁,Exclusive-Lock,X 锁,写锁。 -
根据锁的粒度,可以将锁分为以下三类:
-
表锁,Table-Lock,MyISAM 存储引擎仅支持表锁; -
页锁,Page-Lock,使用相对较少; -
行锁,Row-Lock,InnoDB 存储引擎也支持行锁。 -
根据锁的模式,可以将锁分为以下几种:
-
行锁,Record Lock,锁定一条记录; -
间隙锁,Gap Lock,锁定一个范围,不包括记录本身; -
Next-key Lock,锁定一个范围的记录包括记录本身,Next-key Lock = Record Lock + Gap Lock; -
插入意向锁,Insert Intention Lock,用于行锁和表锁共存。
具体各种类型锁的介绍将在本系列后续文章中逐一介绍。
这里简单介绍下行锁,行锁锁定的是什么,是索引还是数据?
实际上 InnoDB 行锁是通过给索引项加锁实现的,如果没有索引,InnoDB 会通过隐藏的聚簇索引来对记录加锁。
因此如果不通过索引条件检索数据,InnoDB 将对表中所有数据加锁,实际效果与表锁一样。
锁的结构
对一条记录加锁的本质是在内存中创建一个锁结构与之关联(隐式锁除外)。如果有多个锁,保存在链表结构中。
简化后的锁结构示意图如下所示,主要包括 trx 信息与 is_waiting 属性,分别表示锁所在的事务信息与当前事务是否在等待,然后将锁结构与行记录关联。
假设事务 T1 改动了这条记录,就生成了一个锁结构与该记录关联,因此 is_waiting 属性为 false,表示加锁成功。
事务 T1 提交之前, 另一个事务 T2 也想改动这条记录,先去查看有没有锁结构与这条记录关联,发现有一个锁结构与之关联后,也生成了一个锁结构与该记录关联,不过 is_waiting 属性为 true,表示锁等待,直到 T1 提交后释放锁。
更详细的 InnoDB 存储引擎中的事务锁结构如下所示。
其中:
-
锁所在的事务信息:无论表锁还是行锁,都是在事务执行过程中给生成的,因此需要加载是哪个事务生成了这个锁结构;
-
索引信息:对于行锁需要记录加锁的记录属于哪个索引,原因是行锁是给索引项加锁;
-
表锁/行锁信息:
-
对于表锁,记载这是对哪个表加的锁,还有其他的一些信息; -
对于行锁,主要记载三个信息,包括 Space ID 记录所在表空间、Page Number 记录所在页号、 n_bit 表示对哪一条记录加了锁,对于行锁,一条记录对应一个比特位; -
type_node:32 个比特位,记载三部分信息,包括 lock_mode 锁的模式、lock_type 锁的类型和 rec_lock_type 行锁的具体类型:
-
lock_mode,锁的模式,占用低 4 位,十进制的 0、1、2、3、4 分别表示表级共享意向锁 IS、表级排它意向锁 IX、行级共享锁 LOCK_S、行级排它锁 LOCK_X、表级 LOCK_AUTO_INC 自增锁; -
lock_type,锁的类型,占用第 5~8 位,不过现阶段只有第 5 位和第 6 位被使用。其中十进制的 16 和 32 分别表示表级锁与行级锁; -
rec_lock_type,行锁的具体类型,十进制的 0、512、1024、2048 分别表示 LOCK_ORDINARY 即 Next-key Lock、LOCK_GAP 即间隙锁、LOCK_REC_NOT_GAP 即正经记录锁、LOCK_INSERT_INTENTION 即插入意向锁。此外,十进制的 256 表示 LOCK_WAIT,因此当第 9 个比特位为 0 与 1 分别表示当前事务获取到锁与未获取到锁处于等待状态。 -
其他信息:为了更好的管理系统运行过程中生成的各种锁结构而设计了各种哈希表和链表,可以先忽略;
-
一堆比特位:比特位的数量是由上面提到的 n_bits 属性表示,页面中的每条记录在记录头信息中都包含一个 heap_no 属性,伪记录 Infimum 的 heap_no 值为0,Supremum 的 heap_no 值为 1,之后每插入一条记录,heap_no 值就增 1。锁结构最后的一堆比特位就对应着一个页面中的记录,一个比特位映射一个 heap_no。
文中案例update t2 set name='d' where id=1;
这条 update 语句执行时锁结构中信息如下所示。
---TRANSACTION 11309020, ACTIVE 161 sec
2 lock struct(s), heap size 1136, 1 row lock(s), undo log entries 1
TABLE LOCK table `test_zk`.`t2` trx id 11309020 lock mode IX
RECORD LOCKS space id 190 page no 3 n bits 80 index PRIMARY of table `test_zk`.`t2` trx id 11309020 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
其中:
-
Space ID = 190、Page Number = 3、n_bits = 80、index = PRIMARY -
type_mode = LOCK_X | LOCK_REC | LOCK_REC_NOT_GAP = 3 | 32 | 1024 -
heap no 2,表明表中的第一行记录被锁定; -
n_fields 4,含义还不确定。
锁等待时显示 2 lock struct(s),表示 trx->trx_locks 锁链表的长度为2,每个链表节点代表该事务持有的一个锁结构,包括表锁,记录锁以及自增锁等。
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
其中:
-
LOCK WAIT 2 lock struct(s) 表示事务正在等待锁,其中锁链表的长度为 2,并非表示在等待两把锁; -
2 locks 表示 IX 锁和 lock_mode X locks rec but not gap 即 Record Lock。
小技巧
锁等待分析
分析锁等待时,建议在发生锁等待的现场关联查询分析持锁与等锁的事务与 SQL,注意如果锁等待已超时,就看不到了,SQL 如下所示。
select
wt.thread_id waiting_thread_id,
wt.processlist_id waiting_processlist_id,
wt.processlist_time waiting_time,
wt.processlist_info waiting_query,
bt.thread_id blocking_thread_id,
bt.processlist_id blocking_processlist_id,
bt.processlist_time blocking_time,
c.sql_text blocking_query,
concat('kill ',bt.processlist_id, ';') sql_kill_blocking_connection
from information_schema.innodb_lock_waits l join information_schema.innodb_trx b
on b.trx_id = l.blocking_trx_id
join information_schema.innodb_trx w
on w.trx_id = l.requesting_trx_id
join performance_schema.threads wt
on w.trx_mysql_thread_id=wt.processlist_id
join performance_schema.threads bt
on b.trx_mysql_thread_id=bt.processlist_id
join performance_schema.events_statements_current c
on bt.thread_id=c.thread_id G
PS.data_locks
从 MySQL 8.0.1 版本开始,可以通过 performance_schema.data_locks 表查看 SQL 执行过程中需要获取的锁。
select * from performance_schema.data_locks G
上文中提到,只有当事务因为获取不到锁而被阻塞即发生锁等待时 information_schema.innodb_locks 表中才会有记录,而 performance_schema.data_locks 表中即使事务没有被阻塞,也可以看到事务持有的锁,这一点对于锁分析非常有用。
查看 update 这条 SQL 执行需要获取的锁。
mysql> select * from performance_schema.data_locks G
Empty set (0.00 sec)
mysql> update t2 set name='d' where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from performance_schema.data_locks G
*************************** 1. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 140123070938328:1070:140122972540608
ENGINE_TRANSACTION_ID: 2032017
THREAD_ID: 64
EVENT_ID: 26
OBJECT_SCHEMA: test_zk
OBJECT_NAME: t2
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: NULL
OBJECT_INSTANCE_BEGIN: 140122972540608
LOCK_TYPE: TABLE # 表级锁
LOCK_MODE: IX # X 型意向锁
LOCK_STATUS: GRANTED
LOCK_DATA: NULL
*************************** 2. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 140123070938328:8:4:2:140122972537552
ENGINE_TRANSACTION_ID: 2032017
THREAD_ID: 64
EVENT_ID: 26
OBJECT_SCHEMA: test_zk
OBJECT_NAME: t2
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: PRIMARY # 主键索引
OBJECT_INSTANCE_BEGIN: 140122972537552
LOCK_TYPE: RECORD # 行级锁
LOCK_MODE: X,REC_NOT_GAP # X 型记录锁
LOCK_STATUS: GRANTED
LOCK_DATA: 1 # 锁定主键值为1的记录
2 rows in set (0.00 sec)
结果显示 update 操作需要获取两把锁,包括表级别的意向排它锁与行级别 X 锁(RECORD LOCK),与上文中分析结论一致。
上文中查看 INNODB_LOCKS 与 INNODB_LOCK_WAITS 表中均有告警 1 warning,如下所示查看告警。
mysql> show warnings;
+---------+------+------------------------------------------------------------------------------------------+
| Level | Code | Message |
+---------+------+------------------------------------------------------------------------------------------+
| Warning | 1681 | 'INFORMATION_SCHEMA.INNODB_LOCKS' is deprecated and will be removed in a future release. |
+---------+------+------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
mysql> show warnings;
+---------+------+-----------------------------------------------------------------------------------------------+
| Level | Code | Message |
+---------+------+-----------------------------------------------------------------------------------------------+
| Warning | 1681 | 'INFORMATION_SCHEMA.INNODB_LOCK_WAITS' is deprecated and will be removed in a future release. |
+---------+------+-----------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
实际上,这两张表在 5.7.14 版本中已过时,8.0.1 版本中已删除。
This table is deprecated as of MySQL 5.7.14 and is removed in MySQL 8.0.
其中:
-
INFORMATION_SCHEMA.INNODB_LOCKS 被 performance_schema.data_locks 代替; -
INFORMATION_SCHEMA.INNODB_LOCK_WAITS 被 data_lock_waitsdata_lock_waits 代替。
结论
锁本质是是一种并发控制手段,用于解决事务在并发执行时可能引发的一致性问题。
写-写操作会导致脏写,即一个事务覆盖另一个事务未提交的更改,因此需要给写操作加写锁。
InnoDB 存储引擎支持事务与行锁,其中行锁是给索引项加锁。
对一条记录加锁的本质是在内存中创建一个锁结构与之关联(隐式锁除外)。如果有多个锁,保存在链表结构中。
锁结构中主要包括 trx 信息与 is_waiting 属性,分别表示锁所在的事务信息与当前事务是否在等待,然后将锁结构与行记录关联。
InnoDB 中锁的实现是悲观锁,先加锁后访问,因此无论是否获取到锁,都会在内存中生成对应的锁结构,其中 is_waiting 为 false 表示持锁,为 true 表示等锁。
因此,并发 update 会导致锁等待,分析锁等待的方法主要包括:
-
使用 information_schema 数据库中的表获取锁信息,不过要求锁等待现场查看; -
使用 SHOW ENGINE INNODB STATUS 获取锁信息,不过信息不全,因此适合死锁分析。
从 MySQL 8.0.1 版本开始,可以通过 performance_schema.data_locks 表查看 SQL 执行过程中需要获取的锁。即使事务没有被阻塞,也可以看到事务持有的锁,这一点对于锁分析非常有用。
通过查询 performance_schema.data_locks 表,可以明确的看到 update 操作需要获取两把锁,包括表级别的意向排它锁与行级别 X 锁(RECORD LOCK)。
待办
-
锁的类型 -
锁的信息,n_bits、n_fields -
死锁分析 -
事务隔离级别、MVCC 与锁的关系
参考教程
-
《MySQL 是怎样运行的》
-
30 张图搞懂 MySQL行锁 -
阿里二面:怎么解决MySQL死锁问题的? -
InnoDB 层锁、事务、统计信息字典表 | 全方位认识 information_schema
原文始发于微信公众号(丹柿小院):MySQL 并发 update 导致锁等待
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/178652.html