参考文章
《mysql show full processlist 详解》
《MySQL性能分析之show processlist及information_schema.processlist详解》
《Mysql报Deadlock found when trying to get lock; try restarting transaction问题解决》
《MySql Lock wait timeout exceeded该如何处理》
《关于MySQL的lock wait timeout exceeded解决方案》
写在开头:本文为学习后的总结,可能有不到位的地方,错误的地方,欢迎各位指正。
操作系统对死锁的完整定义为:多个进程在运行过程中因争夺某一临界资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。
我们经常遇到的锁问题有2种:
(1)Deadlock found when trying to get lock; try restarting transaction
(2)lock wait timeout exceeded; try restarting transaction
第1种异常是Mysq明确规定的死锁,在官方文档中有非常详细的解释,这里截取一部分,有兴趣的朋友可以移步这里查看 《Deadlocks in InnoDB》
很多文章里习惯把第2种异常称为死锁,但这其实有问题的,我们从他的英文名翻译来
看,lock wait timeout exceeded,锁等待超时,也就是说,事务A先加了锁后,事务B也想加锁,
如果事务A是排他锁,事务B就只能等待,当这个等待时间超过阈值(可通过mysql全局变量查看,
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout')时,就会抛出此异常。说不对的原因在于
锁等待超时 ,可能是因为慢sql等原因。在我司的内部博客中也有DBA做了专门的说明。
不过有1点相同,就是这2中情况都使得我们的事务执行异常(try restarting transaction)
造成锁问题的可能原因很多,这里简单列举下:
(1)执行DML操作没有commit,再执行删除操作就会锁表。
(2)在同一事务内先后对同一条数据进行插入和更新操作。
(3)表索引设计不当,导致数据库出现死锁。
(4)长事物,阻塞DDL,继而阻塞所有同表的后续操作。
补充: 既然提到了锁,那必然要介绍下Mysql中锁(这里只做简单介绍,实际规则要复杂许多)
(1)表锁,在InnoDB与MyISAM中都有实现,执行粒度是整张表,说人话就是
事务A加了表锁,事务B在获取到锁前就动不了这张表
(2)行锁,只在InnODB中实现了,行锁的粒度单行记录,就是锁事务A锁住了某条记录,
事务B可以动这张表的其他记录,但不能动这条记录,行锁的出现提高了mysql的并发能力。(注
意,行锁的前提是命中索引,即这条sql利用到了某个索引才行)
(3)间隙锁,在可重复读(Repeatable read)级别下,使用当前读模式会存在幻读的隐
患,即锁的粒度是行记录的级别,我们虽然不能改这些记录但是可以往表里插入数据。就导致了事
务中前后读取结果不一致,为了解决这一问题就有了间隙锁,将加了行锁的这些记录再两两之间加
上锁,使得新的相关记录无法插入,这就是间隙锁。(这是当前读模式,与之对应的快照读则采用
了多版本并发控制(MVCC)来实现)。
这部分可以看我这篇博客:《mysql之事务、锁、隔离级别与MVCC》
select * from information_schema.innodb_trx
有几个重要的列我们需要了解
trx_id:事务id
trx_status:当前的事务状态,running、lock wait、rolling back or commtting.
trx_started:事务的开始时间
trx_requested_lock_id:事务等待的锁的id(如果事务状态不是lock wait,这个字段是null)
trx_wait_started:事务等待的开始时间
trx_mysql_thread_id:MySQL找那个的进程id,与show processlist中的id值相对应
trx_query:事务运行的SQL语句
trx_operation_state:事务当操作的类型如updating or deleting,stating index read等
死锁问题本地没复现出来,所以网上找了张图,当作参考。
我们可以看到标红的这条事务处于lock wait锁等待状态,当我们在运维时遇到这种就可以使
用 kill + trx_mysql_thread_id 来杀死(图中例子具体脚本为 kill 738178711)。
同时,我们也可以根据trx_started即事务的开始时间判断是否有长事务阻塞等情况。
上面介绍了事务的处理方案,但开头我们也介绍了,不只是事务有锁,我们的DML(insert、
delete、update)操作也会产生锁(锁等待时间通过show variables like 'lock_wait_timeout' 可以
查看,我们发现比事务锁阈值参数少了个开头的InnoDB,这也强调了只有InnoDB中才有事务,而锁
则广泛存在于各种存储引擎中)
mysql为我们提供了下面这个管理工具来查看所有sql的运行情况。
show full processlist
id:线程id
user:显示单前用户,如果不是root,这 个命令就只显示你权限范围内的sql语句。
host:显示这个语句是从哪个ip的哪个端口上发出的,可以用来追踪出问题语句的用户。
db:显示 这个进程目前连接的是哪个数据库。
command:显示当前连接的执行的命令,一般就是休眠(sleep),查询(query),连接 (connect)。
time列,此这个状态持续的时间,单位是秒。
state:显示使用当前连接的sql语句的状态,请注意,state只是语句执行中的某一个状态, 一个sql语句,可能需要经过copying to tmp table,Sorting result,Sending data等状态才可以完成。
info:显示这个sql语句,因为长度有限,所以长的sql语句就显示不全,但是一个判断问题语句的重要依据。
我们可以观察执行时间明显异常的sql,通过killd + id 将其杀死。
注意点:
(1)Time、Info列比较重要,Info列展示当前线程运行SQL,如果为空,
则说明当前线程为sleep状态,而Time列则代表当前状态维持的时长(s),如果Info有具体的SQL,
则代表该SQL运行时长,如果Info为空,则代表线程空闲等待时长,线程每次执行新的SQL,Time都会重新计时。
(2)存在很多sleep状态的线程time列很大,这些sleep线程其实是因为druid连接池的原因,所以一直并未真正关闭,处于空闲等待。
补充个小知识:mysql中每个 mysql 的连接都在一个单独的线程中运行,(所以这里
processlist叫做连接清单或者线程清单都行)我们执行kill命令实际上就是将这个线程杀死。
关于kill的使用可以查看这篇官方文档。《kill statement》
除了show full processlist ,我们还可以使用information_schema.processlist来查看,好处是可以对状态维持时间排序。
select id, db, user, host, command, time, state, info
from information_schema.processlist
order by time desc
展示的信息和show full processlist是一样的,但还有另一个好处,就是可以批量生成kill脚本,这里我生成了kill所有lock状态2分钟的脚本。
select concat('kill ', id, ';')
from information_schema.processlist
where command = 'Locked'
and time > 2*60
order by time desc
这块内容需要对工具展示的内容有一定了解,内容较多这里这里暂不详解,条件允许可以咨询DBA。
show engine innodb status
================================分割线===================================
2021年8月29日补全:详解文章《mysql死锁分析工具show engine innodb status》已补齐,可移步观看。