电子说
生产环境中经常会遇到锁等待与死锁相关的问题,这类问题通常比较紧急,而且由于锁相关影响因素较多,因此分析难度较大。
本文从最简单的一类锁等待开始,即并发 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.innodb_trx 表中存储了 InnoDB 存储引擎当前正在执行的事务信息。
其中:
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
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: 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)
从中可以看到与锁相关的事务,但是无法看到锁的具体类型。
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\\G
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)
从中可以看到具体请求的锁的类型,但是无法区分等锁事务与持锁事务。
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)
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 数据库中的表。
主要信息如下所示。
---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
---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
因此,锁等待分析的结论如下所示:
首先为什么需要锁?
锁本质上是一种并发控制手段,用于解决事务在并发执行时可能引发的一致性问题。
并发事务访问相同数据基本上可以分为以下三种情况:
而 InnoDB 存储引擎支持事务与行锁,并实现了基于 MVCC 的事务并发处理机制。
如下所示,根据不同的维度,可以将锁分为不同的类型。
其中:
具体各种类型锁的介绍将在本系列后续文章中逐一介绍。
这里简单介绍下行锁,行锁锁定的是什么,是索引还是数据?
实际上 InnoDB 行锁是通过给索引项加锁实现的 ,如果没有索引,InnoDB 会通过隐藏的聚簇索引来对记录加锁。
因此如果不通过索引条件检索数据,InnoDB 将对表中所有数据加锁,实际效果与表锁一样。
对一条记录加锁的本质是在内存中创建一个锁结构与之关联(隐式锁除外)。如果有多个锁,保存在链表结构中。
简化后的锁结构示意图如下所示,主要包括 trx 信息与 is_waiting 属性,分别表示锁所在的事务信息与当前事务是否在等待,然后将锁结构与行记录关联。
img
假设事务 T1 改动了这条记录,就生成了一个锁结构与该记录关联,因此 is_waiting 属性为 false,表示加锁成功。
事务 T1 提交之前, 另一个事务 T2 也想改动这条记录,先去查看有没有锁结构与这条记录关联,发现有一个锁结构与之关联后,也生成了一个锁结构与该记录关联,不过 is_waiting 属性为 true,表示锁等待,直到 T1 提交后释放锁。
img
更详细的 InnoDB 存储引擎中的事务锁结构如下所示。
img
其中:
文中案例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
其中:
锁等待时显示 2 lock struct(s),表示 trx->trx_locks 锁链表的长度为2,每个链表节点代表该事务持有的一个锁结构,包括表锁,记录锁以及自增锁等。
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
其中:
分析锁等待时,建议在发生锁等待的现场关联查询分析持锁与等锁的事务与 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
从 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.
其中:
锁本质是是一种并发控制手段,用于解决事务在并发执行时可能引发的一致性问题。
写-写操作会导致脏写,即一个事务覆盖另一个事务未提交的更改,因此需要给写操作加写锁。
InnoDB 存储引擎支持事务与行锁,其中行锁是给索引项加锁。
对一条记录加锁的本质是在内存中创建一个锁结构与之关联(隐式锁除外)。如果有多个锁,保存在链表结构中。
锁结构中主要包括 trx 信息与 is_waiting 属性,分别表示锁所在的事务信息与当前事务是否在等待,然后将锁结构与行记录关联。
InnoDB 中锁的实现是悲观锁,先加锁后访问,因此无论是否获取到锁,都会在内存中生成对应的锁结构,其中 is_waiting 为 false 表示持锁,为 true 表示等锁。
因此,并发 update 会导致锁等待,分析锁等待的方法主要包括:
从 MySQL 8.0.1 版本开始,可以通过 performance_schema.data_locks 表查看 SQL 执行过程中需要获取的锁。即使事务没有被阻塞,也可以看到事务持有的锁,这一点对于锁分析非常有用。
通过查询 performance_schema.data_locks 表,可以明确的看到 update 操作需要获取两把锁,包括表级别的意向排它锁与行级别 X 锁(RECORD LOCK)。
全部0条评论
快来发表一下你的评论吧 !