系列最后一篇介绍MySQL中常见的四类锁阻塞问题 —— 全局读锁、metadata lock(MDL)、表锁、行锁
一、 全局读锁
全局读锁通常由 flush table with read lock; 这类语句添加,这类语句通常在各种备份工具获取一致性备份。或主备切换时使用。还有一种最难排查的情况,如果线上系统权限约束不规范,各种帐号都具有RELOAD权限时,都可以对数据库加全局读锁。
在MySQL 5.7之前的版本,要排查谁持有全局读锁通常在数据库层面很难直接查询到有用数据(innodb_locks表也只能记录innodb层的锁信息,而全局读锁是server层的锁,无法查询到),从5.7开始提供表performance_schema.metadata_locks表记录一些Server层的锁信息(包括全局读锁和MDL锁等)。
下面通过一个示例使用performance_schema来找出谁持有全局读锁。
首先,开启第一个会话,执行全局读锁。
-- 执行加锁语句
flush table with read lock;
Query OK, 0 rows affected (0.00 sec)
-- 查询加锁线程的process id,后续排查过程好对应
root@localhost : sbtest 12:31:48> select connection_id();
+-----------------+
| connection_id() |
+-----------------+
| 4 |
+-----------------+
1 row in set (0.00 sec)
开启第二个会话执行任意dml语句,以update操作为例。
-- 测试查询
select * from sbtest1 limit 1\G;
*************************** 1. row ***************************
id: 21
k: 2483476
c: 09279210219-37745839908-56185699327-79477158641-86711242956-61449540392-42622804506-61031512845-36718422840-11028803849
pad: 96813293060-05308009118-09223341195-19224109585-45598161848
1 row in set (0.00 sec)
ERROR:
No query specified
-- 记下线程id
select connection_id();
+-----------------+
| connection_id() |
+-----------------+
| 5 |
+-----------------+
1 row in set (0.00 sec)
-- 测试更新
update sbtest1 set pad='xxx' where id=21; -- 操作被阻塞
开启第三个会话,进行排查。
select connection_id();
+-----------------+
| connection_id() |
+-----------------+
| 16 |
+-----------------+
1 row in set (0.00 sec)
-- 查询processlist信息,只能看到processid为5的线程State为Waiting for global read lock,表示正在等待全局读锁
show processlist;
+----+-------+---------------------+--------+-------------+-------+---------------------------------------------------------------+------------------------------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+-------+---------------------+--------+-------------+-------+---------------------------------------------------------------+------------------------------------------+
| 3 | qfsys | 192.168.2.168:41042 | NULL | Binlog Dump | 11457 | Master has sent all binlog to slave; waiting for more updates | NULL |
| 4 | root | localhost | sbtest | Sleep | 234 | | NULL |
| 5 | root | localhost | sbtest | Query | 26 | Waiting for global read lock | update sbtest1 set pad='xxx' where id=21 |
| 16 | root | localhost | NULL | Query | 0 | starting | show processlist |
+----+-------+---------------------+--------+-------------+-------+---------------------------------------------------------------+------------------------------------------+
4 rows in set (0.00 sec)
-- 查询information_schema.innodb_locks、innodb_lock_waits、innodb_trx表,发现三个表均为空(因为全局读锁不是InnoDB层而是Server层的锁)
select * from information_schema.innodb_locks;
Empty set, 1 warning (0.00 sec)
select * from information_schema.innodb_lock_waits;
Empty set, 1 warning (0.00 sec)
select * from information_schema.innodb_trx\G
Empty set (0.00 sec)
-- 再使用show engine innodb status;查看一把(只需要看TRANSACTION段落即可),仍然无任何有用的锁信息
root@localhost : (none) 12:59:48> show engine innodb status;
......
------------
TRANSACTIONS
------------
Trx id counter 2527502
Purge done for trx's n:o < 2527500 undo n:o < 0 state: running but idle
History list length 3
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 422099353083504, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 422099353082592, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 422099353081680, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
通过常规手段查询下来无任何有用信息,这个时候,有gdb调试经验的老鸟估计就要开始使用gdb,strace,pstack什么的命令查看MySQL 调用栈、线程信息什么的了,但这对于没有C语言基础的人来说,基本上是看天书。
从5.7版本开始,提供performance_schema.metadata_locks表记录各种Server层的锁信息,下面我们查询该表试试。
-- 持锁会话通常有两个(但为同一个线程),特征如下:OBJECT_TYPE为global和commit、LOCK_TYPE为SHARED
select * from performance_schema.metadata_locks where OWNER_THREAD_ID!=sys.ps_thread_id(connection_id())\G;
*************************** 1. row ***************************
OBJECT_TYPE: GLOBAL <---
OBJECT_SCHEMA: NULL
OBJECT_NAME: NULL
OBJECT_INSTANCE_BEGIN: 140621322913984
LOCK_TYPE: SHARED # 共享锁
LOCK_DURATION: EXPLICIT # 显式
LOCK_STATUS: GRANTED # 已授予
SOURCE: lock.cc:1110
OWNER_THREAD_ID: 94 # 持有锁的内部线程ID为94
OWNER_EVENT_ID: 16
*************************** 2. row ***************************
OBJECT_TYPE: COMMITL <---
OBJECT_SCHEMA: NULL
OBJECT_NAME: NULL
OBJECT_INSTANCE_BEGIN: 140621322926064
LOCK_TYPE: SHARED # 共享锁
LOCK_DURATION: EXPLICIT # 显式
LOCK_STATUS: GRANTED # 已授予
SOURCE: lock.cc:1194
OWNER_THREAD_ID: 94 # 持有锁的内部线程ID为94
OWNER_EVENT_ID: 16
*************************** 3. row ***************************
OBJECT_TYPE: GLOBAL
OBJECT_SCHEMA: NULL
OBJECT_NAME: NULL
OBJECT_INSTANCE_BEGIN: 140621391527216
LOCK_TYPE: INTENTION_EXCLUSIVE # 意向排它锁
LOCK_DURATION: STATEMENT # 语句
LOCK_STATUS: PENDING # 状态为pending,等待锁授予(正在被阻塞)
SOURCE: sql_base.cc:3190
OWNER_THREAD_ID: 95 # 被阻塞的内部线程ID为95
OWNER_EVENT_ID: 38
3 rows in set (0.00 sec)
-- 查看process id为4,5 各自对应的内部线程ID是多少
select sys.ps_thread_id(4);
+---------------------+
| sys.ps_thread_id(4) |
+---------------------+
| 94 | # process id=4的线程对应的内部线程ID正好为94,说明就是process id=4的线程持有了全局读锁
+---------------------+
1 row in set (0.00 sec)
select sys.ps_thread_id(5);
+---------------------+
| sys.ps_thread_id(5) |
+---------------------+
| 95 | # proces id=5的线程对应的内部线程正好是95,说明在等待全局读锁的就是process id=5的线程
+---------------------+
1 row in set (0.00 sec)
如果是生产环境,综合上述信息,通过show processlist信息中对应的process id=4的行记录中找到user、host、db信息,大致判断一下是属于什么业务用途,找相关人员询问清楚,该杀掉就杀掉,顺便讨论下今后如何避免这个问题。
二、 找出谁持有MDL锁
这个问题常见于:执行大查询/大事务操作未提交 -> 执行ddl语句/备份 -> 其他会话执行读写操作,出现大量waiting for metadata lock等待。
后面介绍sys系统库时会看到有一个schema_table_lock_wait(5.7.9新增)视图,可以非常方便地查看MDL等待信息。查看这个视图的定义会发现其数据来源就是performance_schema下的threads、metadata_locks、events_statements_current表。这篇我们先来看利用performance_schema下的表怎么找出谁持有MDL锁。
mdl锁记录对应的instruments为wait/lock/metadata/sql/mdl,默认未启用,对应的consumers为performance_schema.metadata_locks,在setup_consumers只受全局配置项global_instrumentation控制,默认启用。
UPDATE performance_schema.setup_instruments SET ENABLED = 'YES' WHERE NAME = 'wait/lock/metadata/sql/mdl';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
select * from performance_schema.setup_instruments WHERE NAME = 'wait/lock/metadata/sql/mdl';
+----------------------------+---------+-------+
| NAME | ENABLED | TIMED |
+----------------------------+---------+-------+
| wait/lock/metadata/sql/mdl | YES | NO |
+----------------------------+---------+-------+
1 row in set (0.00 sec)
首先,打开两个会话,分别执行如下语句。
-- 会话1,显式开启一个事务,并执行一个update语句更新sbtest1表不提交
begin;
Query OK, 0 rows affected (0.00 sec)
update sbtest1 set pad='yyy' where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
-- 会话2,对sbtest1表执行DDL语句添加一个普通索引
alter table sbtest1 add index i_c(c); -- 被阻塞
另外开启一个会话3,使用show processlist语句查询线程信息,可以发现update语句正在等待MDL锁(Waiting for table metadata lock)。
show processlist;
+----+------+-----------+--------+---------+------+---------------------------------+--------------------------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+------+-----------+--------+---------+------+---------------------------------+--------------------------------------+
| 92 | root | localhost | sbtest | Query | 121 | Waiting for table metadata lock | alter table sbtest1 add index i_c(c) |
| 93 | root | localhost | NULL | Query | 0 | starting | show processlist |
| 94 | root | localhost | sbtest | Sleep | 1078 | | NULL |
+----+------+-----------+--------+---------+------+---------------------------------+--------------------------------------+
3 rows in set (0.00 sec)
查询performance_schema.metadata_locks表查看MDL锁信息
root@localhost : (none) 01:23:05> select * from performance_schema.metadata_locks where OWNER_THREAD_ID!=sys.ps_thread_id(connection_id())\G;
*************************** 1. row ***************************
OBJECT_TYPE: TABLE
OBJECT_SCHEMA: sbtest
OBJECT_NAME: sbtest1
OBJECT_INSTANCE_BEGIN: 139886013386816
LOCK_TYPE: SHARED_WRITE
LOCK_DURATION: TRANSACTION
LOCK_STATUS: GRANTED
SOURCE: sql_parse.cc:5996
OWNER_THREAD_ID: 136
OWNER_EVENT_ID: 721
*************************** 2. row ***************************
OBJECT_TYPE: GLOBAL
OBJECT_SCHEMA: NULL
OBJECT_NAME: NULL
OBJECT_INSTANCE_BEGIN: 139886348911600
LOCK_TYPE: INTENTION_EXCLUSIVE
LOCK_DURATION: STATEMENT
LOCK_STATUS: GRANTED
SOURCE: sql_base.cc:5497
OWNER_THREAD_ID: 134
OWNER_EVENT_ID: 4667
*************************** 3. row ***************************
OBJECT_TYPE: SCHEMA
OBJECT_SCHEMA: sbtest
OBJECT_NAME: NULL
OBJECT_INSTANCE_BEGIN: 139886346748096
LOCK_TYPE: INTENTION_EXCLUSIVE
LOCK_DURATION: TRANSACTION
LOCK_STATUS: GRANTED
SOURCE: sql_base.cc:5482
OWNER_THREAD_ID: 134
OWNER_EVENT_ID: 4667
*************************** 4. row ***************************
OBJECT_TYPE: TABLE
OBJECT_SCHEMA: sbtest
OBJECT_NAME: sbtest1
OBJECT_INSTANCE_BEGIN: 139886346749984
LOCK_TYPE: SHARED_UPGRADABLE
LOCK_DURATION: TRANSACTION
LOCK_STATUS: GRANTED
SOURCE: sql_parse.cc:5996
OWNER_THREAD_ID: 134
OWNER_EVENT_ID: 4669
*************************** 5. row ***************************
OBJECT_TYPE: TABLE
OBJECT_SCHEMA: sbtest
OBJECT_NAME: sbtest1
OBJECT_INSTANCE_BEGIN: 139886348913168
LOCK_TYPE: EXCLUSIVE
LOCK_DURATION: TRANSACTION
LOCK_STATUS: PENDING <-- 被阻塞者
SOURCE: mdl.cc:3891
OWNER_THREAD_ID: 134
OWNER_EVENT_ID: 4748
5 rows in set (0.00 sec)
发现有5行MDL锁记录,第一行为sbtest.sbtest1表的SHARED_WRITE锁,处于GRANTED状态,为136线程持有(对应process id为94)。后续4行中,有sbtest.sbtest1表的SHARED_UPGRADABLE、EXCLUSIVE锁,其中SHARED_UPGRADABLE处于GRANTED状态,EXCLUSIVE处于PENDING状态,为134线程持有(对应process id为92),说明134线程在等待MDL锁。
通过上述数据,我们知道136线程持有了MDL锁,通过show processlist语句的查询结果可以看到process id为94的线程已经长时间处于sleep状态,并不能看到这个线程执行了什么语句。
需要查询一下information_schema.innodb_trx表,确认该线程是否存在没有提交的事务。通过查询该表发现process id为94(trx_mysql_thread_id=94)的线程确实有一个未提交的事务,但并没有太多的有用信息。
select * from information_schema.innodb_trx\G;
*************************** 1. row ***************************
trx_id: 2452892
trx_state: RUNNING
trx_started: 2018-01-14 01:19:25 <-- 事务开始时间
trx_requested_lock_id: NULL
trx_wait_started: NULL
trx_weight: 3
trx_mysql_thread_id: 94 <-- 线程id
......
1 row in set (0.00 sec)
虽然知道了是136线程的事务没有提交导致的134线程发生MDL锁等待,但并不知道136线程正在做什么。我们当然可以kill掉136线程让134线程继续往下执行,但如果不知道136线程在执行什么语句,就无法找到相关的开发人员进行优化,下次还可能再次碰到类似的问题。
我们还可以借助performance_schema.events_statements_current表来查询某个线程正在执行或最后一次执行完成的语句事件信息(这里信息并不一定可靠,因为该表中对于每个线程只能记录当前正在执行和最近一次执行完成的语句事件信息,一旦这个线程执行新的语句,信息就会被覆盖):
select * from performance_schema.events_statements_current where thread_id=136\G;
*************************** 1. row ***************************
THREAD_ID: 136
EVENT_ID: 715
END_EVENT_ID: 887
EVENT_NAME: statement/sql/update
SOURCE: socket_connection.cc:101
......
SQL_TEXT: update sbtest1 set pad='yyy' where id=1
DIGEST: 69f516aa8eaa67fd6e7bfd3352de5d58
DIGEST_TEXT: UPDATE `sbtest1` SET `pad` = ? WHERE `id` = ?
CURRENT_SCHEMA: sbtest
.....
1 row in set (0.00 sec)
从performance_schema.events_statements_current 表的查询信息中,通过SQL_TEXT字段我们可以看到该线程正在执行的SQL语句是什么。如果是生产环境,现在,你可以去找相关的开发人员交涉,下次碰到类似的语句必须及时提交,避免下次再发生类似的问题。
三、 找出谁持有表级锁
表级锁对应的instruments(wait/lock/table/sql/handler)默认启用,对应的consumers为performance_schema.table_handles,在setup_consumers只受全局配置项global_instrumentation控制,默认启用。所以默认情况下设置performance_schema = ON即可。下面我们通过一个示例演示如何找出谁持有表级锁。
首先,开启两个会话,第一个会话对表执行显式加表级锁,第二个会话对该表执行DML语句操作
-- 会话1加表级锁
select connection_id();
+-----------------+
| connection_id() |
+-----------------+
| 18 |
+-----------------+
1 row in set (0.00 sec)
lock table sbtest1 read;
Query OK, 0 rows affected (0.00 sec)
-- 会话2对该表执行update更新
select connection_id();
+-----------------+
| connection_id() |
+-----------------+
| 19 |
+-----------------+
1 row in set (0.00 sec)
update sbtest1 set pad='xxx' where id=1; -- 被阻塞
开启第三个会话,使用show processlist语句查询线程信息,发现更新语句正在等待MDL锁
root@localhost : (none) 02:40:14> show processlist;
+----+-------+---------------------+--------+-------------+-------+---------------------------------------------------------------+-----------------------------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+-------+---------------------+--------+-------------+-------+---------------------------------------------------------------+-----------------------------------------+
| 3 | qfsys | 192.168.2.168:41042 | NULL | Binlog Dump | 18565 | Master has sent all binlog to slave; waiting for more updates | NULL |
| 18 | root | localhost | sbtest | Sleep | 67 | | NULL |
| 19 | root | localhost | sbtest | Query | 51 | Waiting for table metadata lock | update sbtest1 set pad='xxx' where id=1 |
| 20 | root | localhost | NULL | Query | 0 | starting | show processlist |
+----+-------+---------------------+--------+-------------+-------+---------------------------------------------------------------+-----------------------------------------+
4 rows in set (0.00 sec)
可能会想到,既然是等待MDL锁那就按上一节的步骤来查,查询performance_schema.metadata_locks表,记录的顺序代表持有锁的时间顺序
select * from performance_schema.metadata_locks where OWNER_THREAD_ID!=sys.ps_thread_id(connection_id())\G;
*************************** 1. row ***************************
OBJECT_TYPE: TABLE
OBJECT_SCHEMA: sbtest
OBJECT_NAME: sbtest1
OBJECT_INSTANCE_BEGIN: 140622530920576
LOCK_TYPE: SHARED_READ_ONLY
LOCK_DURATION: TRANSACTION
LOCK_STATUS: GRANTED
SOURCE: sql_parse.cc:5996
OWNER_THREAD_ID: 113 -- 内部ID为113的线程被授予SHARED_READ_ONLY锁,持有该锁的线程不允许其他线程修改sbtest1表的数据
OWNER_EVENT_ID: 11
*************************** 2. row ***************************
OBJECT_TYPE: GLOBAL
OBJECT_SCHEMA: NULL
OBJECT_NAME: NULL
OBJECT_INSTANCE_BEGIN: 140620517607728
LOCK_TYPE: INTENTION_EXCLUSIVE
LOCK_DURATION: STATEMENT
LOCK_STATUS: GRANTED
SOURCE: sql_base.cc:3190
OWNER_THREAD_ID: 114 -- 内部ID为114的线程被授予锁INTENTION_EXCLUSIVE,即意向锁IX
OWNER_EVENT_ID: 12
*************************** 3. row ***************************
OBJECT_TYPE: TABLE
OBJECT_SCHEMA: sbtest
OBJECT_NAME: sbtest1
OBJECT_INSTANCE_BEGIN: 140620517607824
LOCK_TYPE: SHARED_WRITE
LOCK_DURATION: TRANSACTION
LOCK_STATUS: PENDING
SOURCE: sql_parse.cc:5996
OWNER_THREAD_ID: 114 -- 内部ID为114的线程正在等待SHARED_WRITE锁被授予
OWNER_EVENT_ID: 12
3 rows in set (0.00 sec)
排查陷入僵局,我们知道MDL锁非常常见,对表的绝大部分的操作都会先对表加MDL锁(根据performance_schema.metadata_locks表中记录的锁信息也不顶用了)。
此时可以尝试去查一些表级别的锁信息,查询performance_schema .table_handles表,如下
select * from performance_schema.table_handles where OWNER_THREAD_ID!=0\G;
*************************** 1. row ***************************
OBJECT_TYPE: TABLE
OBJECT_SCHEMA: sbtest
OBJECT_NAME: sbtest1
OBJECT_INSTANCE_BEGIN: 140622530923216
OWNER_THREAD_ID: 113
OWNER_EVENT_ID: 11
INTERNAL_LOCK: NULL
EXTERNAL_LOCK: READ EXTERNAL -- 发现内部ID为113的线程持有了sbtest1表的READ EXTERNAL表级锁,这也是内部ID为114的线程无法获取到MDL锁的原因
1 row in set (0.00 sec)
通过上述查询到的相关数据可以看出,113线程对sbtest1表显式加了表级读锁,而且长时间处于睡眠状态,但我们并不知道该线程正在执行什么SQL语句,可以通过performance_schema.events_statements_current表查询,如下
select * from performance_schema.events_statements_current where thread_id=113\G;
*************************** 1. row ***************************
THREAD_ID: 113
EVENT_ID: 10
END_EVENT_ID: 10
EVENT_NAME: statement/sql/lock_tables
SOURCE: socket_connection.cc:101
TIMER_START: 18503556405463000
TIMER_END: 18503556716572000
TIMER_WAIT: 311109000
LOCK_TIME: 293000000
SQL_TEXT: lock table sbtest1 read -- 这里可以看到,内部ID为113的线程对表sbtest1执行了加读锁语句
DIGEST: 9f987e807ca36e706e33275283b5572b
DIGEST_TEXT: LOCK TABLE `sbtest1` READ
CURRENT_SCHEMA: sbtest
......
1 row in set (0.00 sec)
如果是生产环境,可以去找相关的开发人员确认,如果没有什么特殊操作,可以尝试着杀掉这个线程。
如何知道内部ID 113对应的process id是多少呢?可以通过performance_schema.threads表查询。注意113并不是process id,千万别乱杀。
select processlist_id from performance_schema.threads where thread_id=113;
+----------------+
| processlist_id |
+----------------+
| 18 |
+----------------+
1 row in set (0.00 sec)
kill 18;
Query OK, 0 rows affected (0.00 sec)
show processlist;
+----+-------+---------------------+--------+-------------+-------+---------------------------------------------------------------+------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+-------+---------------------+--------+-------------+-------+---------------------------------------------------------------+------------------+
| 3 | qfsys | 192.168.2.168:41042 | NULL | Binlog Dump | 18994 | Master has sent all binlog to slave; waiting for more updates | NULL |
| 19 | root | localhost | sbtest | Sleep | 480 | | NULL |
| 20 | root | localhost | NULL | Query | 0 | starting | show processlist |
+----+-------+---------------------+--------+-------------+-------+---------------------------------------------------------------+------------------+
3 rows in set (0.00 sec)
-- 返回执行update语句的会话2,语句已经执行成功
update sbtest1 set pad='xxx' where id=1;
Query OK, 0 rows affected (7 min 50.23 sec)
Rows matched: 0 Changed: 0 Warnings: 0
四、 找出谁持有行级锁
该案例中涉及的performance_schema.data_lock表在MySQL 8.0中新增,8.0之前的版本中不支持,这里仅作为针对MySQL 5.7的performance_schema的一个延伸学习。
如果一个事务长时间未提交,虽然可以从information_schema.innodb_trx,performance_schema.events_transactions_current等表中查询到相应的事务信息,但却无从知道这个事务持有了哪些锁。虽然information_schema.innodb_locks表是用于记录事务锁信息的,但需要在事务发生锁等待时该表才会记录锁信息。
从8.0开始,在performance_schema中提供了一个data_locks表用于记录任意事务的锁信息(同时废弃了information_schema.innodb_locks表),不需要有锁等待关系存在(注意,该表中只记录innodb的存储引擎层的锁)。
首先,我们在8.0中打开一个会话(会话1),显式开启一个事务
select * from t_luoxiaobo limit 1;
+----+------+---------------------+
| id | test | datet_time |
+----+------+---------------------+
| 2 | 1 | 2017-09-06 01:11:59 |
+----+------+---------------------+
1 row in set (0.00 sec)
begin;
Query OK, 0 rows affected (0.00 sec)
update t_luoxiaobo set datet_time=now() where id=2;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
打开另外一个会话(会话2)查询data_locks表
select * from data_locks\G;
*************************** 1. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 55562:62
ENGINE_TRANSACTION_ID: 55562
THREAD_ID: 54 # 持有线程内部ID
EVENT_ID: 85
OBJECT_SCHEMA: xiaoboluo # 库名
OBJECT_NAME: t_luoxiaobo # 表名
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: NULL # 索引名称
OBJECT_INSTANCE_BEGIN: 140439793477144
LOCK_TYPE: TABLE # 表级锁
LOCK_MODE: IX # IX锁
LOCK_STATUS: GRANTED # 被授予状态
LOCK_DATA: NULL
*************************** 2. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 55562:2:4:2
ENGINE_TRANSACTION_ID: 55562
THREAD_ID: 54 # 持有锁线程内部ID
EVENT_ID: 85
OBJECT_SCHEMA: xiaoboluo # 库名
OBJECT_NAME: t_luoxiaobo #表名
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: PRIMARY # 索引为主键
OBJECT_INSTANCE_BEGIN: 140439793474104
LOCK_TYPE: RECORD # 行锁
LOCK_MODE: X # 排它锁
LOCK_STATUS: GRANTED # 被授予状态
LOCK_DATA: 2 # 被锁定的数据记录,这里的记录对应的是 INDEX_NAME: PRIMARY 的value
2 rows in set (0.00 sec)
从查询结果可以看到,有两行锁记录,第一行是对表t_luoxiaobo的IX锁,第二行为使用主键索引的X锁记录锁,状态均为GRANTED
现在,我们模拟两个DML发生锁等待的场景。新开一个会话(会话3),在会话1中的事务未提交的情况下,会话3对表t_luoxiaobo执行同样的操作
begin;
Query OK, 0 rows affected (0.00 sec)
update t_luoxiaobo set datet_time=now() where id=2; -- 被阻塞
回到会话2中查询data_locks表,发现有4行锁记录
select * from performance_schema.data_locks\G;
*************************** 1. row ***************************
......
THREAD_ID: 55
......
LOCK_TYPE: TABLE
LOCK_MODE: IX
LOCK_STATUS: GRANTED
LOCK_DATA: NULL
*************************** 2. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 55563:2:4:2
ENGINE_TRANSACTION_ID: 55563
THREAD_ID: 55 # 内部线程ID
EVENT_ID: 8
OBJECT_SCHEMA: xiaoboluo
OBJECT_NAME: t_luoxiaobo
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: PRIMARY # 发生锁的索引名称
OBJECT_INSTANCE_BEGIN: 140439793480168
LOCK_TYPE: RECORD # 记录锁
LOCK_MODE: X # 排它锁
LOCK_STATUS: WAITING # 正在等待锁被授予
LOCK_DATA: 2 # 锁定的索引value,这里与内部ID为54线程持有的主键值为2的X锁完全一样,说明这里就是被内部ID为54线程阻塞了
*************************** 3. row ***************************
......
THREAD_ID: 54
.......
LOCK_TYPE: TABLE
LOCK_MODE: IX
LOCK_STATUS: GRANTED
LOCK_DATA: NULL
*************************** 4. row ***************************
......
THREAD_ID: 54
EVENT_ID: 85
OBJECT_SCHEMA: xiaoboluo
OBJECT_NAME: t_luoxiaobo
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: PRIMARY
OBJECT_INSTANCE_BEGIN: 140439793474104
LOCK_TYPE: RECORD
LOCK_MODE: X
LOCK_STATUS: GRANTED
LOCK_DATA: 2
4 rows in set (0.00 sec)
这里并不能很直观地查看到锁等待关系,我们可以使用sys.innodb_lock_waits视图查看
root@localhost : (none) 09:44:52> select * from sys.innodb_lock_waits\G;
*************************** 1. row ***************************
wait_started: 2018-01-14 21:51:59
wait_age: 00:00:11 <-- 等待时间
wait_age_secs: 11
locked_table: `xiaoboluo`.`t_luoxiaobo` <-- 表信息
locked_table_schema: xiaoboluo
locked_table_name: t_luoxiaobo
locked_table_partition: NULL
locked_table_subpartition: NULL
locked_index: PRIMARY <-- 主键字段
locked_type: RECORD <-- 行锁
waiting_trx_id: 55566
waiting_trx_started: 2018-01-14 21:51:59
waiting_trx_age: 00:00:11
waiting_trx_rows_locked: 1
waiting_trx_rows_modified: 0
waiting_pid: 8 <-- 被阻塞的线程id
waiting_query: update t_luoxiaobo set datet_time=now() where id=2 <-- 被阻塞者正执行的sql语句
waiting_lock_id: 55566:2:4:2
waiting_lock_mode: X <-- 锁模式
blocking_trx_id: 55562
blocking_pid: 7 <-- 阻塞的线程id
blocking_query: NULL <-- 阻塞者正执行的sql语句(已经执行完了,但没提交)
blocking_lock_id: 55562:2:4:2
blocking_lock_mode: X
blocking_trx_started: 2018-01-14 21:34:44
blocking_trx_age: 00:17:26 <-- 持锁时间
blocking_trx_rows_locked: 1
blocking_trx_rows_modified: 1
sql_kill_blocking_query: KILL QUERY 7
sql_kill_blocking_connection: KILL 7 <-- kill阻塞者的语句
1 row in set (0.02 sec)
MySQL 5.7版本中也可以使用sys.innodb_lock_waits视图查询,但是在8.0中,该视图联结查询的表不同(把之前版本中使用的information_schema.innodb_locks和information_schema.innodb_lock_waits表替换为了performance_schema.data_locks和performance_schema.data_lock_waits表)。
5.6及其之前的版本中默认没有sys库,可以使用如下语句代替:
SELECT r.trx_wait_started AS wait_started,
TIMEDIFF(NOW(), r.trx_wait_started) AS wait_age,
TIMESTAMPDIFF(SECOND, r.trx_wait_started, NOW()) AS wait_age_secs,
rl.lock_table AS locked_table,
rl.lock_index AS locked_index,
rl.lock_type AS locked_type,
r.trx_id AS waiting_trx_id,
r.trx_started as waiting_trx_started,
TIMEDIFF(NOW(), r.trx_started) AS waiting_trx_age,
r.trx_rows_locked AS waiting_trx_rows_locked,
r.trx_rows_modified AS waiting_trx_rows_modified,
r.trx_mysql_thread_id AS waiting_pid,
sys.format_statement(r.trx_query) AS waiting_query,
rl.lock_id AS waiting_lock_id,
rl.lock_mode AS waiting_lock_mode,
b.trx_id AS blocking_trx_id,
b.trx_mysql_thread_id AS blocking_pid,
sys.format_statement(b.trx_query) AS blocking_query,
bl.lock_id AS blocking_lock_id,
bl.lock_mode AS blocking_lock_mode,
b.trx_started AS blocking_trx_started,
TIMEDIFF(NOW(), b.trx_started) AS blocking_trx_age,
b.trx_rows_locked AS blocking_trx_rows_locked,
b.trx_rows_modified AS blocking_trx_rows_modified,
CONCAT('KILL QUERY ', b.trx_mysql_thread_id) AS sql_kill_blocking_query,
CONCAT('KILL ', b.trx_mysql_thread_id) AS sql_kill_blocking_connection
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
INNER JOIN information_schema.innodb_locks bl ON bl.lock_id = w.blocking_lock_id
INNER JOIN information_schema.innodb_locks rl ON rl.lock_id = w.requested_lock_id
ORDER BY r.trx_wait_started;
参考:《MySQL 性能优化金字塔法则》