一. 大纲


20191219 10:10:10,234 | com.alibaba.druid.filter.logging.Log4jFilter.statementLogError(Log4jFilter.java:152) | ERROR | {conn-10593, pstmt-38675} execute error. update xxx set xxx = ? , xxx = ? where RowGuid = ?

com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction


之前在如何有效排查解决MySQL行锁等待超时问题 帖子中介绍了如何监控解决行锁超时报错,当时介绍的监控方案主要是以shell脚本 + general_log来捕获行锁等待信息,后来想了想这种方式比较麻烦,因此改良了下,使用Event + Procedure的方法定时在MySQl内进行监控,然后将行锁等待信息记录到日志表中,这样可以省去登陆服务器的过程,更为简便。

大家如果在项目中遇到该报错,可以先用以下方式进行监控,再通过日志表分析。

二. 步骤

该方式只适用于公司bin包部署的MySQL,不适用于其他自行安装或RDS上的MySQL。

2.1. 创建库

--最好新建个库,用现有库也可以

create database `innodb_monitor`;

2.2. 创建存储过程

dbeaver中创建存储过程需要选择执行SQL脚本,是否无法创建。

MySQL5.7与MySQL8.0的存储过程略有不同,主要是系统表名发生了变化,请针对版本创建存储过程。

2.2.1. MySQL5.7存储过程

-- <MySQL5.7版本> ---
use innodb_monitor;
delimiter go
CREATE PROCEDURE pro_innodb_lock_wait_check()
BEGIN
declare wait_rows int;
set group_concat_max_len = 1024000;
CREATE TABLE IF NOT EXISTS `innodb_lock_wait_log` (
`report_time` datetime DEFAULT NULL,
`waiting_id` int(11) DEFAULT NULL,
`blocking_id` int(11) DEFAULT NULL,
`duration` varchar(50) DEFAULT NULL,
`state` varchar(50) DEFAULT NULL,
`waiting_query` longtext DEFAULT NULL,
`blocking_current_query` longtext DEFAULT NULL,
`blocking_thd_last_query` longtext,
`thread_id` int(11) DEFAULT NULL
);
select count(*) into wait_rows from information_schema.innodb_lock_waits ;
if wait_rows > 0 THEN
insert into `innodb_lock_wait_log` SELECT now(),r.trx_mysql_thread_id waiting_id,b.trx_mysql_thread_id blocking_id,concat(timestampdiff(SECOND,r.trx_wait_started,CURRENT_TIMESTAMP()),'s') AS duration,
t.processlist_command state,r.trx_query waiting_query,b.trx_query blocking_current_query,group_concat(left(h.sql_text,10000) order by h.TIMER_START DESC SEPARATOR ';\n') As blocking_thd_query_history,thread_id
FROM information_schema.innodb_lock_waits w JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id
LEFT JOIN performance_schema.threads t on t.processlist_id = b.trx_mysql_thread_id LEFT JOIN performance_schema.events_statements_history h USING(thread_id) group by thread_id,r.trx_id order by r.trx_wait_started;
end if;
END
go

2.2.2. MySQL8.0存储过程

-- <MySQL8.0版本> ---
use innodb_monitor;
delimiter go
CREATE PROCEDURE pro_innodb_lock_wait_check()
BEGIN
declare wait_rows int;
set group_concat_max_len = 1024000;
CREATE TABLE IF NOT EXISTS `innodb_lock_wait_log` (
`report_time` datetime DEFAULT NULL,
`waiting_id` int(11) DEFAULT NULL,
`blocking_id` int(11) DEFAULT NULL,
`duration` varchar(50) DEFAULT NULL,
`state` varchar(50) DEFAULT NULL,
`waiting_query` longtext DEFAULT NULL,
`blocking_current_query` longtext DEFAULT NULL,
`blocking_thd_last_query` longtext,
`thread_id` int(11) DEFAULT NULL
);
select count(*) into wait_rows from performance_schema.data_lock_waits;
if wait_rows > 0 THEN
insert into `innodb_lock_wait_log` SELECT now(),r.trx_mysql_thread_id waiting_id,b.trx_mysql_thread_id blocking_id,concat(timestampdiff(SECOND,r.trx_wait_started,CURRENT_TIMESTAMP()),'s') AS duration,
t.processlist_command state,r.trx_query waiting_query,b.trx_query blocking_current_query,group_concat(left(h.sql_text,10000) order by h.TIMER_START DESC SEPARATOR ';\n') As blocking_thd_query_history,thread_id
FROM performance_schema.data_lock_waits w
JOIN information_schema.innodb_trx b ON b.trx_id = w.BLOCKING_ENGINE_TRANSACTION_ID
JOIN information_schema.innodb_trx r ON r.trx_id = w.REQUESTING_ENGINE_TRANSACTION_ID
LEFT JOIN performance_schema.threads t on t.processlist_id = b.trx_mysql_thread_id LEFT JOIN performance_schema.events_statements_history h USING(thread_id) group by thread_id,r.trx_id order by r.trx_wait_started;
end if;
END
go

2.3. 创建事件 dbeaver中创建事件需要选择执行SQL脚本,是否无法创建。

默认每隔5秒执行一次,持续监控7天,结束后会自动删除事件,可以自定义修改保留时长。当事件超过7天被自动删除后,可以重新建立事件继续运行。

use innodb_monitor;
delimiter go
CREATE EVENT `event_innodb_lock_wait_check`
ON SCHEDULE EVERY 5 SECOND
STARTS CURRENT_TIMESTAMP
ENDS CURRENT_TIMESTAMP + INTERVAL 7 DAY
ON COMPLETION NOT PRESERVE
ENABLE
DO
call pro_innodb_lock_wait_check();
go

2.4. 事件启停 默认不需要进行如下命令,事件创建后会自动执行,如需手动停止Event则可以参考如下命令。

--1为全局开启事件,0为全局关闭事件

mysql > SET GLOBAL event_scheduler = 1;

--临时关闭事件

mysql > ALTER EVENT event_innodb_lock_wait_check DISABLE;

--关闭开启事件

mysql > ALTER EVENT event_innodb_lock_wait_check ENABLE;

三. 日志表 最后我们可以根据日志表innodb_lock_wait_log中的记录,分析行锁等待原因,其中主要有2种场景:

blocking_current_query列不为空:说明阻塞事务处于运行状态,这时候需要分析当前运行SQL是否存在性能问题。

blocking_current_query为空,state为Sleep:此时阻塞事务处于挂起状态(即不在运行SQL),此时需要通过分析blocking_thd_last_query分析挂起事务上下文,注意该列中的SQL为时间降序,即从下往上执行(默认只能保留10条记录)。

下图是手动模拟的行锁等待场景,因为blocking_current_query为空,说明阻塞事务处于挂起状态,从blocking_thd_last_query列可以看到阻塞事务之前执行过的SQL,而Waiting_query列则代表当前被阻塞的SQL语句,往往也是web日志中执行超时的报错SQL。