快速定位加锁 SQL

首先我们创建一个测试表:

  1. mysql> create table t1(id decimal,v_name varchar(10));
  2. mysql> insert into t1 values(1,'a'),(2,'b'),(3,'c');
  3. mysql> select * from t1;
  4. +------+--------+
  5. | id | v_name |
  6. +------+--------+
  7. | 1 | a |
  8. | 2 | b |
  9. | 3 | c |
  10. +------+--------+
  11. 3 rows in set (0.00 sec)

在会话 1 中开启事务,更新 id=1 的数据:

  1. mysql> begin;
  2. mysql> update t1 set v_name='aa' where id=1;

在会话 2 中开启另一个事务,删除 id=1 的数据:

  1. mysql> begin;
  2. mysql> delete from t1 where id=1;

此时会话 2 被锁定,出现锁等待。

我们再开一个会话 3,查查当前的 processlist,看看是否能发现什么?

  1. mysql> show processlist;
  2. +----+------+-----------+------+---------+------+----------+---------------------------+
  3. | Id | User | Host | db | Command | Time | State | Info |
  4. +----+------+-----------+------+---------+------+----------+---------------------------+
  5. | 38 | root | localhost | test | Sleep | 5 | | NULL |
  6. | 41 | root | localhost | test | Query | 2 | updating | delete from t1 where id=1 |
  7. | 42 | root | localhost | NULL | Query | 0 | starting | show processlist |
  8. +----+------+-----------+------+---------+------+----------+---------------------------+

可以看到 delete 这个 SQL 的进程在执行中,并没有发现其他有价值的内容,那锁在哪里了。接下来我们就一步步的定位出加锁的 SQL。

1)定位锁等待

  1. mysql> select * from information_schema.innodb_lock_waits;
  2. +-------------------+-------------------+-----------------+------------------+
  3. | requesting_trx_id | requested_lock_id | blocking_trx_id | blocking_lock_id |
  4. +-------------------+-------------------+-----------------+------------------+
  5. | 2207 | 2207:28:3:7 | 2206 | 2206:28:3:7 |
  6. +-------------------+-------------------+-----------------+------------------+
  7. 1 row in set, 1 warning (0.00 sec)

结果显示有一个锁等待。

2)定位锁

  1. mysql> select * from information_schema.innodb_locks;
  2. +-------------+-------------+-----------+-----------+-------------+-----------------+------------+-----------+----------+----------------+
  3. | lock_id | lock_trx_id | lock_mode | lock_type | lock_table | lock_index | lock_space | lock_page | lock_rec | lock_data |
  4. +-------------+-------------+-----------+-----------+-------------+-----------------+------------+-----------+----------+----------------+
  5. | 2207:28:3:7 | 2207 | X | RECORD | `test`.`t1` | GEN_CLUST_INDEX | 28 | 3 | 7 | 0x000000000211 |
  6. | 2206:28:3:7 | 2206 | X | RECORD | `test`.`t1` | GEN_CLUST_INDEX | 28 | 3 | 7 | 0x000000000211 |
  7. +-------------+-------------+-----------+-----------+-------------+-----------------+------------+-----------+----------+----------------+
  8. 2 rows in set, 1 warning (0.00 sec)

结果显示有两个锁相关内容。

3)定位事务

  1. mysql> select trx_id,trx_started,trx_requested_lock_id,trx_query,trx_mysql_thread_id from information_schema.innodb_trx;
  2. +--------+---------------------+-----------------------+---------------------------+---------------------+
  3. | trx_id | trx_started | trx_requested_lock_id | trx_query | trx_mysql_thread_id |
  4. +--------+---------------------+-----------------------+---------------------------+---------------------+
  5. | 2207 | 2021-01-18 15:18:11 | 2207:28:3:7 | delete from t1 where id=1 | 41 |
  6. | 2206 | 2021-01-18 15:18:08 | NULL | NULL | 38 |
  7. +--------+---------------------+-----------------------+---------------------------+---------------------+
  8. 2 rows in set (0.01 sec)

结果有两个事务,MySQL 事务线程 id 为 38 和 41,很直观的看到 41 是我们的 delete 事务,被 38 锁定。

4)定位线程

  1. mysql> select * from performance_schema.threads where processlist_id=38;
  2. +-----------+---------------------------+------------+----------------+------------------+------------------+----------------+---------------------+------------------+-------------------+------------------+------------------+------+--------------+---------+-----------------+--------------+
  3. | THREAD_ID | NAME | TYPE | PROCESSLIST_ID | PROCESSLIST_USER | PROCESSLIST_HOST | PROCESSLIST_DB | PROCESSLIST_COMMAND | PROCESSLIST_TIME | PROCESSLIST_STATE | PROCESSLIST_INFO | PARENT_THREAD_ID | ROLE | INSTRUMENTED | HISTORY | CONNECTION_TYPE | THREAD_OS_ID |
  4. +-----------+---------------------------+------------+----------------+------------------+------------------+----------------+---------------------+------------------+-------------------+------------------+------------------+------+--------------+---------+-----------------+--------------+
  5. | 63 | thread/sql/one_connection | FOREGROUND | 38 | root | localhost | test | Sleep | 35 | NULL | NULL | NULL | NULL | YES | YES | Socket | 15070 |
  6. +-----------+---------------------------+------------+----------------+------------------+------------------+----------------+---------------------+------------------+-------------------+------------------+------------------+------+--------------+---------+-----------------+--------------+
  7. 1 row in set (0.00 sec)

结果找到 MySQL 事务线程 38 对应的服务器线程 63。

5)定位加锁 SQL

  1. mysql> select * from performance_schema.events_statements_current where thread_id=63;
  2. +-----------+----------+--------------+----------------------+--------------------------+---------------------+---------------------+------------+-----------+--------------------------------------+----------------------------------+----------------------------------------------+----------------+-------------+---------------+-------------+-----------------------+-------------+-------------------+------------------------------------------+--------+----------+---------------+-----------+---------------+-------------------------+--------------------+------------------+------------------------+--------------+--------------------+-------------+-------------------+------------+-----------+-----------+---------------+--------------------+------------------+--------------------+---------------------+
  3. | THREAD_ID | EVENT_ID | END_EVENT_ID | EVENT_NAME | SOURCE | TIMER_START | TIMER_END | TIMER_WAIT | LOCK_TIME | SQL_TEXT | DIGEST | DIGEST_TEXT | CURRENT_SCHEMA | OBJECT_TYPE | OBJECT_SCHEMA | OBJECT_NAME | OBJECT_INSTANCE_BEGIN | MYSQL_ERRNO | RETURNED_SQLSTATE | MESSAGE_TEXT | ERRORS | WARNINGS | ROWS_AFFECTED | ROWS_SENT | ROWS_EXAMINED | CREATED_TMP_DISK_TABLES | CREATED_TMP_TABLES | SELECT_FULL_JOIN | SELECT_FULL_RANGE_JOIN | SELECT_RANGE | SELECT_RANGE_CHECK | SELECT_SCAN | SORT_MERGE_PASSES | SORT_RANGE | SORT_ROWS | SORT_SCAN | NO_INDEX_USED | NO_GOOD_INDEX_USED | NESTING_EVENT_ID | NESTING_EVENT_TYPE | NESTING_EVENT_LEVEL |
  4. +-----------+----------+--------------+----------------------+--------------------------+---------------------+---------------------+------------+-----------+--------------------------------------+----------------------------------+----------------------------------------------+----------------+-------------+---------------+-------------+-----------------------+-------------+-------------------+------------------------------------------+--------+----------+---------------+-----------+---------------+-------------------------+--------------------+------------------+------------------------+--------------+--------------------+-------------+-------------------+------------+-----------+-----------+---------------+--------------------+------------------+--------------------+---------------------+
  5. | 63 | 32 | 32 | statement/sql/update | socket_connection.cc:101 | 2757904906303653000 | 2757904906543381000 | 239728000 | 145000000 | update t1 set v_name='aa' where id=1 | 356a053ffb5eae2a35981b05090faa01 | UPDATE `t1` SET `v_name` = ? WHERE `id` = ? | test | NULL | NULL | NULL | NULL | 0 | 00000 | Rows matched: 1 Changed: 0 Warnings: 0 | 0 | 0 | 0 | 0 | 3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | NULL | NULL | 0 |
  6. +-----------+----------+--------------+----------------------+--------------------------+---------------------+---------------------+------------+-----------+--------------------------------------+----------------------------------+----------------------------------------------+----------------+-------------+---------------+-------------+-----------------------+-------------+-------------------+------------------------------------------+--------+----------+---------------+-----------+---------------+-------------------------+--------------------+------------------+------------------------+--------------+--------------------+-------------+-------------------+------------+-----------+-----------+---------------+--------------------+------------------+--------------------+---------------------+
  7. 1 row in set (0.00 sec)

在结果中我们找到了加锁的 update 的 SQL 语句。

ALTER TALBE 进度监控

当我们对一张大表执行了一个 ALTER TABLE 操作,执行了很久也不知道是否执行完成。对于这种情况,我们可以通过 MySQL 提供的数据字典进行任务监控。MySQL 在 5.7 版本开始,对数据字典的使用有了很大的改进,使用上更加的方便,提供的能力也更高。performance_schema 可以查询事务信息、获取元数据锁、跟踪事件、统计内存使用情况等等。

想要使用这个能力,我们需要开启几个功能:

Enable the stage/innodb/alter% instruments

  1. mysql> UPDATE performance_schema.setup_instruments
  2. -> SET ENABLED = 'YES'
  3. -> WHERE NAME LIKE 'stage/innodb/alter%';
  4. Query OK, 0 rows affected (0.01 sec)
  5. Rows matched: 7 Changed: 0 Warnings: 0

Enable the stage event consumer tables

  1. mysql> UPDATE performance_schema.setup_consumers
  2. -> SET ENABLED = 'YES'
  3. -> WHERE NAME ='events_stages_current';
  4. Query OK, 1 row affected (0.00 sec)
  5. Rows matched: 1 Changed: 1 Warnings: 0
  6. mysql> UPDATE performance_schema.setup_consumers
  7. -> SET ENABLED = 'YES'
  8. -> WHERE NAME ='events_stages_history';
  9. Query OK, 1 row affected (0.01 sec)
  10. Rows matched: 1 Changed: 1 Warnings: 0
  11. mysql> UPDATE performance_schema.setup_consumers
  12. -> SET ENABLED = 'YES'
  13. -> WHERE NAME ='events_stages_history_long';
  14. Query OK, 1 row affected (0.00 sec)
  15. Rows matched: 1 Changed: 1 Warnings: 0

功能开启后,当我们对一张大表执行 ALTER TABLE 操作后,可以执行如下监控 SQL 语句:

  1. mysql> select * from performance_schema.events_stages_current\G;
  2. *************************** 1. row ***************************
  3. THREAD_ID: 28
  4. EVENT_ID: 14
  5. END_EVENT_ID: NULL
  6. EVENT_NAME: stage/innodb/alter table (read PK and internal sort)
  7. SOURCE:
  8. TIMER_START: 159726265417733000
  9. TIMER_END: 159819571346680000
  10. TIMER_WAIT: 93305928947000
  11. WORK_COMPLETED: 118256
  12. WORK_ESTIMATED: 302958
  13. NESTING_EVENT_ID: 13
  14. NESTING_EVENT_TYPE: STATEMENT
  15. 1 row in set (0.00 sec)
  16. ......
  17. mysql> select * from performance_schema.events_stages_current\G;
  18. *************************** 1. row ***************************
  19. THREAD_ID: 28
  20. EVENT_ID: 14
  21. END_EVENT_ID: NULL
  22. EVENT_NAME: stage/innodb/alter table (read PK and internal sort)
  23. SOURCE:
  24. TIMER_START: 159726265417733000
  25. TIMER_END: 159910492100061000
  26. TIMER_WAIT: 184226682328000
  27. WORK_COMPLETED: 230688
  28. WORK_ESTIMATED: 302958
  29. NESTING_EVENT_ID: 13
  30. NESTING_EVENT_TYPE: STATEMENT
  31. 1 row in set (0.01 sec)

多执行几次,可以发现数据是有变化的,这些内容代表了什么呢?

  • THREAD_ID:线程 ID
  • EVENT_ID:事件 ID
  • END_EVENT_ID:结束事件 ID
  • EVENT_NAME:事件名称,说明了当前执行的事件
  • SOURCE:源码位置
  • TIMER_START:事件开始时间(皮秒)
  • TIMER_END:事件结束时间(皮秒,如果没有执行完成,时间就是当前之间)
  • TIMER_WAIT:事件等待事件(皮秒)
  • WORK_COMPLETED:任务完成情况
  • WORK_ESTIMATED:任务估算情况
  • NESTING_EVENT_ID:事件对应的父事件 ID
  • NESTING_EVENT_TYPE:父事件类型(STATEMENT、STAGE、WAIT)

总结一下,如果我们想查看事件任务完成情况:

  1. mysql> SELECT pt.INFO, ec.THREAD_ID, ec.EVENT_NAME, ec.WORK_COMPLETED, ec.WORK_ESTIMATED, pt.STATE FROM performance_schema.events_stages_current ec left join performance_schema.threads th on ec.thread_id = th.thread_id left join information_schema.PROCESSLIST pt on th.PROCESSLIST_ID = pt.ID where pt.INFO like 'ALTER%';
  2. +-------------------------------------------+-----------+------------------------------------------------------+----------------+----------------+----------------+
  3. | INFO | THREAD_ID | EVENT_NAME | WORK_COMPLETED | WORK_ESTIMATED | STATE |
  4. +-------------------------------------------+-----------+------------------------------------------------------+----------------+----------------+----------------+
  5. | alter table sbtest.sbtest1 add d char(20) | 28 | stage/innodb/alter table (read PK and internal sort) | 201496 | 308223 | altering table |
  6. +-------------------------------------------+-----------+------------------------------------------------------+----------------+----------------+----------------+
  7. 1 row in set (0.25 sec)

如果我们想查看任务完成事件:

  1. mysql> select stmt.sql_text as sql_text, concat(work_completed, '/' , work_estimated) as progress, (stage.timer_end - stmt.timer_start) / 1e12 as current_seconds, (stage.timer_end - stmt.timer_start) / 1e12 * (work_estimated-work_completed) / work_completed as remaining_seconds from performance_schema.events_stages_current stage, performance_schema.events_statements_current stmt where stage.thread_id = stmt.thread_id and stage.nesting_event_id = stmt.event_id;
  2. +-------------------------------------------+---------------+-----------------+--------------------+
  3. | sql_text | progress | current_seconds | remaining_seconds |
  4. +-------------------------------------------+---------------+-----------------+--------------------+
  5. | alter table sbtest.sbtest1 add d char(20) | 135192/308223 | 102.461512532 | 131.13954949201502 |
  6. +-------------------------------------------+---------------+-----------------+--------------------+
  7. 1 row in set (0.00 sec)

这样我们通过 MySQL 的数据字典就可以很直观地看到 ALTER TABLE 的执行情况了。

大事务监控

在 MySQL 中提供了 PERFORMANCE_SCHEMA.events_statements_history 这个视图。这个视图里记录了最近运行的 SQL 语句,于是就有了下面的排查 SQL 语句:

  1. SELECT
  2. ps.id 'PROCESS ID',
  3. ps.USER,
  4. ps.HOST,
  5. esh.EVENT_ID,
  6. trx.trx_started,
  7. esh.event_name 'EVENT NAME',
  8. esh.sql_text 'SQL',
  9. ps.time
  10. FROM
  11. PERFORMANCE_SCHEMA.events_statements_history esh
  12. JOIN PERFORMANCE_SCHEMA.threads th ON esh.thread_id = th.thread_id
  13. JOIN information_schema.PROCESSLIST ps ON ps.id = th.processlist_id
  14. LEFT JOIN information_schema.innodb_trx trx ON trx.trx_mysql_thread_id = ps.id
  15. WHERE
  16. trx.trx_id IS NOT NULL
  17. AND ps.USER != 'SYSTEM_USER'
  18. ORDER BY
  19. esh.EVENT_ID;

执行示例如下:

  1. +------------+------+-----------+----------+---------------------+------------------------------+-------------------------------------------+------+
  2. | PROCESS ID | USER | HOST | EVENT_ID | trx_started | EVENT NAME | SQL | time |
  3. +------------+------+-----------+----------+---------------------+------------------------------+-------------------------------------------+------+
  4. | 530 | root | localhost | 7 | 2020-09-01 02:52:01 | statement/com/Init DB | NULL | 196 |
  5. | 530 | root | localhost | 8 | 2020-09-01 02:52:01 | statement/sql/show_databases | show databases | 196 |
  6. | 530 | root | localhost | 9 | 2020-09-01 02:52:01 | statement/sql/show_tables | show tables | 196 |
  7. | 530 | root | localhost | 10 | 2020-09-01 02:52:01 | statement/com/Field List | NULL | 196 |
  8. | 530 | root | localhost | 11 | 2020-09-01 02:52:01 | statement/sql/show_tables | show tables | 196 |
  9. | 530 | root | localhost | 12 | 2020-09-01 02:52:01 | statement/sql/select | select * from t_test | 196 |
  10. | 530 | root | localhost | 13 | 2020-09-01 02:52:01 | statement/sql/begin | begin | 196 |
  11. | 530 | root | localhost | 14 | 2020-09-01 02:52:01 | statement/sql/update | update t_test set paymont=100 where id=99 | 196 |
  12. | 530 | root | localhost | 15 | 2020-09-01 02:52:01 | statement/sql/select | select * from t_test where id=98 | 196 |
  13. | 530 | root | localhost | 16 | 2020-09-01 02:52:01 | statement/sql/update | update t_test set paymont=101 where id=97 | 196 |
  14. +------------+------+-----------+----------+---------------------+------------------------------+-------------------------------------------+------+
  15. 10 rows in set (0.34 sec)

慢查询监控

MySQL 的慢查询日志是 MySQL 提供的一种日志记录,它用来记录在 MySQL 中响应时间超过阈值的语句,具体指运行时间超过 long_query_time 值的 SQL,则会被记录到慢查询日志中。long_query_time 的默认值为 10,表示运行 10S 以上的语句。默认情况下,MySQL 数据库并不启动慢查询日志,需要我们手动来设置这个参数,当然,如果不是调优需要的话,一般不建议启动该参数,因为开启慢查询日志会或多或少带来一定的性能影响。慢查询日志支持将日志记录写入文件,也支持将日志记录写入数据库表。

慢查询相关参数:

slow_query_log:是否开启慢查询日志,值为 1 表示开启,0 表示关闭,默认是关闭的。
image.png
long_query_time:慢查询阈值,当查询时间大于设定的阈值时,记录慢查询日志,默认为 10 秒。精读可以配置到微秒级。
image.png
log_queries_not_using_indexes:是否将未使用索引的查询也被记录到慢查询日志中。
image.png
log_output:日志存储方式,值为 FILE 表示将日志存入文件(默认),值为 TABLE 表示将日志存入数据库,此时日志信息会被写入到 mysql.slow_log 表。MySQL 支持同时配置两种存储方式,配置时以逗号隔开。
image.png

在生产环境中,如果要手工分析慢查询日志,查找、分析 SQL,显然是个体力活,为此,MySQL 提供了慢查询日志分析工具 mysqldumpslow。查看 mysqldumpslow 的帮助信息:

  1. [root@DB-Server ~]# mysqldumpslow --help
  2. Usage: mysqldumpslow [ OPTS... ] [ LOGS... ]
  3. Parse and summarize the MySQL slow query log. Options are
  4. --verbose verbose
  5. --debug debug
  6. --help write this text to standard output
  7. -v verbose
  8. -d debug
  9. -s ORDER what to sort by (al, at, ar, c, l, r, t), 'at' is default
  10. al: average lock time
  11. ar: average rows sent
  12. at: average query time
  13. c: count
  14. l: lock time
  15. r: rows sent
  16. t: query time
  17. -r reverse the sort order (largest last instead of first)
  18. -t NUM just show the top n queries
  19. -a don't abstract all numbers to N and strings to 'S'
  20. -n NUM abstract numbers with at least n digits within names
  21. -g PATTERN grep: only consider stmts that include this string
  22. -h HOSTNAME hostname of db server for *-slow.log filename (can be wildcard),
  23. default is '*', i.e. match all
  24. -i NAME name of server instance (if using mysql.server startup script)
  25. -l don't subtract lock time from total time

其中的参数含义如下:

s:表示按照何种方式排序 c:访问计数 l:锁定时间 r:返回记录 t:查询时间 al:平均锁定时间 a:平均返回记录数 at:平均查询时间 t:是 top n 的意思,即为返回前面多少条的数据 g:后边可以写一个正则匹配模式,大小写不敏感的

比如,想要得到返回记录集最多的 10 个 SQL:

  1. mysqldumpslow -s r -t 10 /usr/local/mysql/mysql06_slow.log

得到访问次数最多的 10 个 SQL:

  1. mysqldumpslow -s c -t 10 /usr/local/mysql/mysql06_slow.log