引言
生产环境中经常会遇到锁等待与死锁相关的问题,这类问题通常比较紧急,而且由于锁相关影响因素较多,因此分析难度较大。
本文从最简单的一类锁等待开始,即并发 update 导致锁等待。
介绍
如果相同的 update 同时执行会发生什么呢?
实际上会发生锁等待,生产环境中就遇到过这种案例,并发 update 导致锁等待。
死锁建立在锁等待的基础上,因此需要先理解锁等待的机制与分析思路。本文通过一个最简单的并发 update 介绍锁等待的分析方法。
模拟
首先,声明事务隔离级别为 RR(REPEATABLE-READ)。
流程
两个 session 分别在开启事务的前提下执行相同的 update 语句导致锁等待。
其中超时时间由系统参数 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_trx\\G*************************** 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: 1trx_last_foreign_key_error: NULL trx_adaptive_hash_latched: 0 trx_adaptive_hash_timeout: 0 trx_is_read_only: 0trx_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: 1trx_last_foreign_key_error: NULL trx_adaptive_hash_latched: 0 trx_adaptive_hash_timeout: 0 trx_is_read_only: 0trx_autocommit_non_locking: 02 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_locks\\GEmpty 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:2lock_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:2lock_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 # 主键值为12 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=1blocking_trx_id: 11309020blocking_thread: 1134 blocking_query: NULL1 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=1sql_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链表,这些链表的总长度等于64History list length 64# 各个事务的具体信息LIST OF TRANSACTIONS FOR EACH SESSION:# not started 空闲事务,表示事务已经提交并且没有再发起影响事务的语句---TRANSACTION 422165848318464, not started0 lock struct(s), heap size 1136, 0 row lock(s)---TRANSACTION 422165848316640, not started0 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 waitingRecord 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 waitingRecord 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 1MySQL 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 gapRecord 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 readLOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)update t2 set name='d' where id=1TABLE LOCK table `test_zk`.`t2` trx id 11309021 lock mode IXRECORD 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 waitingRecord lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
- 11309020 事务分别持有 t2 表的表级别意向排它锁与主键索引上的行级别 X 锁(RECORD LOCK),没有间隙锁。
---TRANSACTION 11309020, ACTIVE 161 sec2 lock struct(s), heap size 1136, 1 row lock(s), undo log entries 1TABLE LOCK table `test_zk`.`t2` trx id 11309020 lock mode IXRECORD 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 gapRecord lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
因此,锁等待分析的结论如下所示:
- update 操作需要获取两把锁,包括表级别的意向排它锁与行级别 X 锁(RECORD LOCK);
- 并发 update 时由于意向锁之间兼容,而行级 X 锁之间冲突,导致发生锁等待。
原理
锁
首先为什么需要锁?
锁本质上是一种并发控制手段,用于解决事务在并发执行时可能引发的一致性问题。
并发事务访问相同数据基本上可以分为以下三种情况:
- 读-读,相互不影响,因此允许;
- 写-写,会导致脏写,因此不允许,通过给记录加锁实现;
- 读-写或写-读,会导致脏读、不可重复读、幻读。解决方案主要分两种:
而 InnoDB 存储引擎支持事务与行锁,并实现了基于 MVCC 的事务并发处理机制。
锁的类型
如下所示,根据不同的维度,可以将锁分为不同的类型。
其中:
- 根据加锁机制,实际上就是锁的实现方式,可以将锁分为以下两类:
- 根据兼容性,可以将锁分为以下两类:
- 根据锁的粒度,可以将锁分为以下三类:
- 根据锁的模式,可以将锁分为以下几种:
具体各种类型锁的介绍将在本系列后续文章中逐一介绍。
这里简单介绍下行锁,行锁锁定的是什么,是索引还是数据?
实际上 InnoDB 行锁是通过给索引项加锁实现的 ,如果没有索引,InnoDB 会通过隐藏的聚簇索引来对记录加锁。
因此如果不通过索引条件检索数据,InnoDB 将对表中所有数据加锁,实际效果与表锁一样。
锁的结构
对一条记录加锁的本质是在内存中创建一个锁结构与之关联(隐式锁除外)。如果有多个锁,保存在链表结构中。
简化后的锁结构示意图如下所示,主要包括 trx 信息与 is_waiting 属性,分别表示锁所在的事务信息与当前事务是否在等待,然后将锁结构与行记录关联。
img
假设事务 T1 改动了这条记录,就生成了一个锁结构与该记录关联,因此 is_waiting 属性为 false,表示加锁成功。
事务 T1 提交之前, 另一个事务 T2 也想改动这条记录,先去查看有没有锁结构与这条记录关联,发现有一个锁结构与之关联后,也生成了一个锁结构与该记录关联,不过 is_waiting 属性为 true,表示锁等待,直到 T1 提交后释放锁。
img
更详细的 InnoDB 存储引擎中的事务锁结构如下所示。
img
其中:
- 锁所在的事务信息:无论表锁还是行锁,都是在事务执行过程中给生成的,因此需要加载是哪个事务生成了这个锁结构;
- 索引信息:对于行锁需要记录加锁的记录属于哪个索引,原因是行锁是给索引项加锁;
- 表锁/行锁信息:
- type_node:32 个比特位,记载三部分信息,包括 lock_mode 锁的模式、lock_type 锁的类型和 rec_lock_type 行锁的具体类型:
- 其他信息:为了更好的管理系统运行过程中生成的各种锁结构而设计了各种哈希表和链表,可以先忽略;
- 一堆比特位:比特位的数量是由上面提到的 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 sec2 lock struct(s), heap size 1136, 1 row lock(s), undo log entries 1TABLE LOCK table `test_zk`.`t2` trx id 11309020 lock mode IXRECORD 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 gapRecord 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_connectionfrom information_schema.innodb_lock_waits l join information_schema.innodb_trx b on b.trx_id = l.blocking_trx_idjoin information_schema.innodb_trx w on w.trx_id = l.requesting_trx_idjoin performance_schema.threads wt on w.trx_mysql_thread_id=wt.processlist_idjoin performance_schema.threads bt on b.trx_mysql_thread_id=bt.processlist_idjoin 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 \\GEmpty 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: 0mysql> select * from performance_schema.data_locks \\G*************************** 1. row *************************** ENGINE: INNODB ENGINE_LOCK_ID: 140123070938328:1070:140122972540608ENGINE_TRANSACTION_ID: 2032017 THREAD_ID: 64 EVENT_ID: 26 OBJECT_SCHEMA: test_zk OBJECT_NAME: t2 PARTITION_NAME: NULL SUBPARTITION_NAME: NULL INDEX_NAME: NULLOBJECT_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:140122972537552ENGINE_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 与锁的关系