参考:MySQL锁表后快速解决方法与锁
这个作者的MySQL专栏值得看一下:http://weikeqin.com/categories/mysql/
依次执行1-6步,运行第6步生成的语句即可。
如果特别着急,运行 1 2 6 步 以及第6步生成的kill语句 即可。
第1步 查看表是否在使用。
show open tables where in_use > 0 ;
如果查询结果为空。则证明表没有在使用。结束。
如果查询结果不为空,继续后续的步骤。
mysql> show open tables where in_use > 0 ; +----------+-------+--------+-------------+ | Database | Table | In_use | Name_locked | +----------+-------+--------+-------------+ | test | t | 1 | 0 | +----------+-------+--------+-------------+ 1 row in set (0.00 sec)
第2步 查看数据库当前的进程,看一下有无正在执行的慢SQL记录线程。
show processlist;
show processlist 是显示用户正在运行的线程,需要注意的是,除了 root 用户能看到所有正在运行的线程外,其他用户都只能看到自己正在运行的线程,看不到其它用户正在运行的线程。
第3步 当前运行的所有事务
SELECT * FROM information_schema.INNODB_TRX;
第4步 当前出现的锁
SELECT * FROM information_schema.INNODB_LOCKs;
第5步 锁等待的对应关系
SELECT * FROM information_schema.INNODB_LOCK_waits;
看事务表INNODB_TRX,里面是否有正在锁定的事务线程,看看ID是否在show processlist里面的sleep线程中,如果是,就证明这个sleep的线程事务一直没有commit或者rollback而是卡住了,我们需要手动kill掉。
搜索的结果是在事务表发现了很多任务,这时候最好都kill掉。
第6步 批量删除事务表中的事务
这里用的方法是:通过information_schema.processlist表中的连接信息生成需要处理掉的MySQL连接的语句临时文件,然后执行临时文件中生成的指令。
SELECT concat('KILL ',id,';') FROM information_schema.processlist p INNER JOIN information_schema.INNODB_TRX x ON p.id=x.trx_mysql_thread_id WHERE db='test';
记得修改对应的数据库名。
这个语句执行后结果如下:
mysql> SELECT concat('KILL ',id,';') FROM information_schema.processlist p INNER JOIN information_schema.INNODB_TRX x ON p.id=x.trx_mysql_thread_id WHERE db='test'; +------------------------+ | concat('KILL ',id,';') | +------------------------+ | KILL 42; | | KILL 40; | +------------------------+ 2 rows in set (0.00 sec)
执行结果里的两个kill语句即可解决锁表。
其他操作:
查看最近死锁的日志:如果使用select * from table for update时,上面的参数无法看到锁的情况,只有在show engine innodb status能查到。
show engine innodb status
当前所有锁的现状
show status like 'innodb_row_lock_%'; +-------------------------------+-----------+ | Variable_name | Value | +-------------------------------+-----------+ | Innodb_row_lock_current_waits | 2 | | Innodb_row_lock_time | 334377476 | | Innodb_row_lock_time_avg | 50678 | | Innodb_row_lock_time_max | 51974 | | Innodb_row_lock_waits | 6598 | +-------------------------------+-----------+ # 解释如下: Innodb_row_lock_current_waits : 当前等待锁的数量 Innodb_row_lock_time : 系统启动到现在,锁定的总时间长度 Innodb_row_lock_time_avg : 每次平均锁定的时间 Innodb_row_lock_time_max : 最长一次锁定时间 Innodb_row_lock_waits : 系统启动到现在总共锁定的次数
information_schema库中增加了三个关于锁的表(MEMORY引擎):
innodb_trx ## 当前运行的所有事务 innodb_locks ## 当前出现的锁 innodb_lock_waits ## 锁等待的对应关系
表的详细结构
desc information_schema.innodb_locks; +-------------+---------------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +-------------+---------------------+------+-----+---------+-------+ | lock_id | varchar(81) | NO | | | |#锁ID | lock_trx_id | varchar(18) | NO | | | |#拥有锁的事务ID | lock_mode | varchar(32) | NO | | | |#锁模式 | lock_type | varchar(32) | NO | | | |#锁类型 | lock_table | varchar(1024) | NO | | | |#被锁的表 | lock_index | varchar(1024) | YES | | NULL | |#被锁的索引 | lock_space | bigint(21) unsigned | YES | | NULL | |#被锁的表空间号 | lock_page | bigint(21) unsigned | YES | | NULL | |#被锁的页号 | lock_rec | bigint(21) unsigned | YES | | NULL | |#被锁的记录号 | lock_data | varchar(8192) | YES | | NULL | |#被锁的数据 +-------------+---------------------+------+-----+---------+-------+ desc information_schema.innodb_lock_waits; +-------------------+-------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +-------------------+-------------+------+-----+---------+-------+ | requesting_trx_id | varchar(18) | NO | | | |#请求锁的事务ID | requested_lock_id | varchar(81) | NO | | | |#请求锁的锁ID | blocking_trx_id | varchar(18) | NO | | | |#当前拥有锁的事务ID | blocking_lock_id | varchar(81) | NO | | | |#当前拥有锁的锁ID +-------------------+-------------+------+-----+---------+-------+ desc information_schema.innodb_trx; +----------------------------+---------------------+------+-----+---------------------+-------+ | Field | Type | Null | Key | Default | Extra | +----------------------------+---------------------+------+-----+---------------------+-------+ | trx_id | varchar(18) | NO | | | |#事务ID | trx_state | varchar(13) | NO | | | |#事务状态: | trx_started | datetime | NO | | 0000-00-00 00:00:00 ||#事务开始时间; | trx_requested_lock_id | varchar(81) | YES | | NULL ||#innodb_locks.lock_id | trx_wait_started | datetime | YES | | NULL | |#事务开始等待的时间 | trx_weight | bigint(21) unsigned | NO | | 0 | |# | trx_mysql_thread_id | bigint(21) unsigned | NO | | 0 ||#事务线程ID | trx_query | varchar(1024) | YES | | NULL | |#具体SQL语句 | trx_operation_state | varchar(64) | YES | | NULL ||#事务当前操作状态 | trx_tables_in_use | bigint(21) unsigned | NO | | 0 ||#事务中有多少个表被使用 | trx_tables_locked | bigint(21) unsigned | NO | | 0 ||#事务拥有多少个锁 | trx_lock_structs | bigint(21) unsigned | NO | | 0 | |# | trx_lock_memory_bytes | bigint(21) unsigned | NO | | 0 ||#事务锁住的内存大小(B) | trx_rows_locked | bigint(21) unsigned | NO | | 0 ||#事务锁住的行数 | trx_rows_modified | bigint(21) unsigned | NO | | 0 ||#事务更改的行数 | trx_concurrency_tickets | bigint(21) unsigned | NO | | 0 ||#事务并发票数 | trx_isolation_level | varchar(16) | NO | | | |#事务隔离级别 | trx_unique_checks | int(1) | NO | | 0 | |#是否唯一性检查 | trx_foreign_key_checks | int(1) | NO | | 0 | |#是否外键检查 | trx_last_foreign_key_error | varchar(256) | YES | | NULL ||#最后的外键错误 | trx_adaptive_hash_latched | int(1) | NO | | 0 | |# | trx_adaptive_hash_timeout | bigint(21) unsigned | NO | | 0 ||# +----------------------------+---------------------+------+-----+---------------------+-------+
创建表t并插入2条数据
CREATE TABLE `t` ( `id` int(11) NOT NULL, `c` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB; INSERT INTO `t` (id, c) VALUES (1, 1), (2, 1);
准备多个shell模拟锁表
可以看到我打开三个shell,用root创建了三个连接,分别是 15 40 41
mysql> show processlist ; +----+-------------+-----------------+------+---------+------+--------------------------+------------------+----------+ | Id | User | Host | db | Command | Time | State | Info | Progress | +----+-------------+-----------------+------+---------+------+--------------------------+------------------+----------+ | 1 | system user | | NULL | Daemon | NULL | InnoDB purge worker | NULL | 0.000 | | 2 | system user | | NULL | Daemon | NULL | InnoDB purge worker | NULL | 0.000 | | 3 | system user | | NULL | Daemon | NULL | InnoDB purge coordinator | NULL | 0.000 | | 4 | system user | | NULL | Daemon | NULL | InnoDB purge worker | NULL | 0.000 | | 5 | system user | | NULL | Daemon | NULL | InnoDB shutdown handler | NULL | 0.000 | | 15 | root | localhost:49914 | NULL | Query | 0 | Init | show processlist | 0.000 | +----+-------------+-----------------+------+---------+------+--------------------------+------------------+----------+ 6 rows in set (0.00 sec)
mysql> mysql> show processlist ; +----+-------------+-----------------+------+---------+------+--------------------------+------------------+----------+ | Id | User | Host | db | Command | Time | State | Info | Progress | +----+-------------+-----------------+------+---------+------+--------------------------+------------------+----------+ | 1 | system user | | NULL | Daemon | NULL | InnoDB purge worker | NULL | 0.000 | | 2 | system user | | NULL | Daemon | NULL | InnoDB purge worker | NULL | 0.000 | | 3 | system user | | NULL | Daemon | NULL | InnoDB purge coordinator | NULL | 0.000 | | 4 | system user | | NULL | Daemon | NULL | InnoDB purge worker | NULL | 0.000 | | 5 | system user | | NULL | Daemon | NULL | InnoDB shutdown handler | NULL | 0.000 | | 15 | root | localhost:49914 | NULL | Query | 0 | Init | show processlist | 0.000 | | 40 | root | localhost:50872 | test | Sleep | 64 | | NULL | 0.000 | | 41 | root | localhost:50888 | test | Sleep | 5 | | NULL | 0.000 | +----+-------------+-----------------+------+---------+------+--------------------------+------------------+----------+ 8 rows in set (0.00 sec)
模拟锁表
在第一个shell里观察
在第二个shell里执行 start transaction; delete from t where c=1 ; 故意打开事务,然后执行语句不提交,占用写锁。
在第三个shell里执行 delete from t where c=1 ; 执行删除语句,造成锁表。
这个时候 session3在等待session2释放写锁。这个时候已经锁表了。
如果再在 第三个shell里执行 delete from t where c=2 ;
在 第二个shell里执行 delete from t where c=1 ;
就会相互等待,造成死锁。
步骤一:查看是否锁表
可以看到下面的查询语句有结果,确实是锁表了。
mysql> show open tables where in_use > 0 ; +----------+-------+--------+-------------+ | Database | Table | In_use | Name_locked | +----------+-------+--------+-------------+ | test | t | 1 | 0 | +----------+-------+--------+-------------+ 1 row in set (0.00 sec)
步骤二:查看数据库当前的进程
mysql> show processlist ; +----+-------------+-----------------+------+---------+------+--------------------------+-------------------------+----------+ | Id | User | Host | db | Command | Time | State | Info | Progress | +----+-------------+-----------------+------+---------+------+--------------------------+-------------------------+----------+ | 1 | system user | | NULL | Daemon | NULL | InnoDB purge worker | NULL | 0.000 | | 2 | system user | | NULL | Daemon | NULL | InnoDB purge worker | NULL | 0.000 | | 3 | system user | | NULL | Daemon | NULL | InnoDB purge coordinator | NULL | 0.000 | | 4 | system user | | NULL | Daemon | NULL | InnoDB purge worker | NULL | 0.000 | | 5 | system user | | NULL | Daemon | NULL | InnoDB shutdown handler | NULL | 0.000 | | 15 | root | localhost:49914 | NULL | Query | 0 | Init | show processlist | 0.000 | | 40 | root | localhost:50872 | test | Sleep | 15 | | NULL | 0.000 | | 41 | root | localhost:50888 | test | Query | 11 | Updating | delete from t where c=1 | 0.000 | +----+-------------+-----------------+------+---------+------+--------------------------+-------------------------+----------+ 8 rows in set (0.00 sec)
步骤三:当前运行的所有事务
mysql> SELECT * FROM information_schema.INNODB_TRX; +--------+-----------+---------------------+-----------------------+---------------------+------------+---------------------+-------------------------+---------------------+-------------------+-------------------+------------------+-----------------------+-----------------+-------------------+-------------------------+---------------------+-------------------+------------------------+----------------------------+------------------+----------------------------+ | trx_id | trx_state | trx_started | trx_requested_lock_id | trx_wait_started | trx_weight | trx_mysql_thread_id | trx_query | trx_operation_state | trx_tables_in_use | trx_tables_locked | trx_lock_structs | trx_lock_memory_bytes | trx_rows_locked | trx_rows_modified | trx_concurrency_tickets | trx_isolation_level | trx_unique_checks | trx_foreign_key_checks | trx_last_foreign_key_error | trx_is_read_only | trx_autocommit_non_locking | +--------+-----------+---------------------+-----------------------+---------------------+------------+---------------------+-------------------------+---------------------+-------------------+-------------------+------------------+-----------------------+-----------------+-------------------+-------------------------+---------------------+-------------------+------------------------+----------------------------+------------------+----------------------------+ | 23312 | LOCK WAIT | 2019-09-05 23:16:18 | 23312:78:3:2 | 2019-09-05 23:16:18 | 2 | 41 | delete from t where c=1 | starting index read | 1 | 1 | 2 | 1136 | 1 | 0 | 0 | REPEATABLE READ | 1 | 1 | NULL | 0 | 0 | | 23311 | RUNNING | 2019-09-05 23:16:13 | NULL | NULL | 3 | 40 | NULL | NULL | 0 | 1 | 2 | 1136 | 3 | 1 | 0 | REPEATABLE READ | 1 | 1 | NULL | 0 | 0 | +--------+-----------+---------------------+-----------------------+---------------------+------------+---------------------+-------------------------+---------------------+-------------------+-------------------+------------------+-----------------------+-----------------+-------------------+-------------------------+---------------------+-------------------+------------------------+----------------------------+------------------+----------------------------+ 2 rows in set (0.00 sec)
步骤四:当前出现的锁
mysql> SELECT * FROM information_schema.INNODB_LOCKs; +--------------+-------------+-----------+-----------+------------+------------+------------+-----------+----------+-----------+ | lock_id | lock_trx_id | lock_mode | lock_type | lock_table | lock_index | lock_space | lock_page | lock_rec | lock_data | +--------------+-------------+-----------+-----------+------------+------------+------------+-----------+----------+-----------+ | 23312:78:3:2 | 23312 | X | RECORD | `test`.`t` | PRIMARY | 78 | 3 | 2 | 1 | | 23311:78:3:2 | 23311 | X | RECORD | `test`.`t` | PRIMARY | 78 | 3 | 2 | 1 | +--------------+-------------+-----------+-----------+------------+------------+------------+-----------+----------+-----------+ 2 rows in set (0.00 sec)
步骤五:锁等待的对应关系
mysql> SELECT * FROM information_schema.INNODB_LOCK_waits; +-------------------+-------------------+-----------------+------------------+ | requesting_trx_id | requested_lock_id | blocking_trx_id | blocking_lock_id | +-------------------+-------------------+-----------------+------------------+ | 23312 | 23312:78:3:2 | 23311 | 23311:78:3:2 | +-------------------+-------------------+-----------------+------------------+ 1 row in set (0.00 sec)
步骤六: 删除事务表中的事务
mysql> SELECT p.id, p.time, i.trx_id, i.trx_state, p.info FROM INFORMATION_SCHEMA.PROCESSLIST p, INFORMATION_SCHEMA.INNODB_TRX i WHERE p.id = i.trx_mysql_thread_id AND i.trx_state = 'LOCK WAIT'; +----+------+--------+-----------+-------------------------+ | id | time | trx_id | trx_state | info | +----+------+--------+-----------+-------------------------+ | 41 | 27 | 23312 | LOCK WAIT | delete from t where c=1 | +----+------+--------+-----------+-------------------------+ 1 row in set (0.01 sec)
步骤七:kill掉锁表的语句
这儿有两种观点,一种是只kill掉后面等待的那个语句。还有一种是把两个语句都kill掉。这个根据实际情况处理。
mysql> kill 41 ; Query OK, 0 rows affected (0.00 sec) mysql> SELECT p.id, p.time, i.trx_id, i.trx_state, p.info FROM INFORMATION_SCHEMA.PROCESSLIST p, INFORMATION_SCHEMA.INNODB_TRX i WHERE p.id = i.trx_mysql_thread_id AND i.trx_state = 'LOCK WAIT'; Empty set (0.01 sec)
杀掉41
mysql> show processlist ; +----+-------------+-----------------+------+---------+------+--------------------------+------------------+----------+ | Id | User | Host | db | Command | Time | State | Info | Progress | +----+-------------+-----------------+------+---------+------+--------------------------+------------------+----------+ | 1 | system user | | NULL | Daemon | NULL | InnoDB purge worker | NULL | 0.000 | | 2 | system user | | NULL | Daemon | NULL | InnoDB purge worker | NULL | 0.000 | | 3 | system user | | NULL | Daemon | NULL | InnoDB purge coordinator | NULL | 0.000 | | 4 | system user | | NULL | Daemon | NULL | InnoDB purge worker | NULL | 0.000 | | 5 | system user | | NULL | Daemon | NULL | InnoDB shutdown handler | NULL | 0.000 | | 15 | root | localhost:49914 | NULL | Query | 0 | Init | show processlist | 0.000 | | 40 | root | localhost:50872 | test | Sleep | 56 | | NULL | 0.000 | +----+-------------+-----------------+------+---------+------+--------------------------+------------------+----------+ 7 rows in set (0.00 sec)
然后到第3个shell窗口查看,可以看到
mysql> delete from t where c=1 ; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
因为第3个shell里执行的语句被kill掉了。
到这儿可以看到死锁解决了。
但其实有个问题。第3个shell里的语句被kill掉了。但第2个shell里的语句还在执行。如果第二个shell里的事务不提交或者kill,在第3个shell里执行删除语句还会造成锁表。
第二种观点的办法
SELECT p.id, p.time, x.trx_id, x.trx_state, p.info FROM INFORMATION_SCHEMA.PROCESSLIST p, INFORMATION_SCHEMA.INNODB_TRX x WHERE p.id = x.trx_mysql_thread_id ; mysql> SELECT p.id, p.time, x.trx_id, x.trx_state, p.info FROM INFORMATION_SCHEMA.PROCESSLIST p, INFORMATION_SCHEMA.INNODB_TRX x WHERE p.id = x.trx_mysql_thread_id ; +----+------+--------+-----------+-------------------------+ | id | time | trx_id | trx_state | info | +----+------+--------+-----------+-------------------------+ | 42 | 3 | 23317 | LOCK WAIT | delete from t where c=1 | | 40 | 1792 | 23311 | RUNNING | NULL | +----+------+--------+-----------+-------------------------+ 2 rows in set (0.01 sec)
然后同时杀掉 40 42 就可以。
当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁。
当出现死锁以后,有两种策略:
第一种策略是:直接进入等待,直到超时。这个超时时间可以通过参数 innodb_lock_wait_timeout 来设置。
在 InnoDB 中,innodb_lock_wait_timeout 的默认值是 50s,意味着如果采用第一个策略,当出现死锁以后,第一个被锁住的线程要过 50s 才会超时退出,然后其他线程才有可能继续执行。对于在线服务来说,这个等待时间往往是无法接受的。
第二种策略是:发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑。
优化方法1:打散
可以考虑通过将一行改成逻辑上的多行来减少锁冲突。还是以影院账户为例,可以考虑放在多条记录上,比如 10 个记录,影院的账户总额等于这 10 个记录的值的总和。这样每次要给影院账户加金额的时候,随机选其中一条记录来加。这样每次冲突概率变成原来的 1/10,可以减少锁等待个数,也就减少了死锁检测的 CPU 消耗。
优化方法2:冲突行后置
如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁的申请时机尽量往后放。
例子:
假设你负责实现一个电影票在线交易业务,顾客 A 要在影院 B 购买电影票。我们简化一点,这个业务需要涉及到以下操作:
从顾客 A 账户余额中扣除电影票价; 给影院 B 的账户余额增加这张电影票价; 记录一条交易日志。
试想如果同时有另外一个顾客 C 要在影院 B 买票,那么这两个事务冲突的部分就是语句 2 了。因为它们要更新同一个影院账户的余额,需要修改同一行数据。
根据两阶段锁协议,不论你怎样安排语句顺序,所有的操作需要的行锁都是在事务提交的时候才释放的。所以,如果你把语句 2 安排在最后,比如按照 3、1、2 这样的顺序,那么影院账户余额这一行的锁时间就最少。这就最大程度地减少了事务之间的锁等待,提升了并发度。
备份一般都会在备库上执行,你在用–single-transaction 方法做逻辑备份的过程中,如果主库上的一个小表做了一个 DDL,比如给一个表上加了一列。这时候,从备库上会看到什么现象呢?
假设这个 DDL 是针对表 t1 的, 这里我把备份过程中几个关键的语句列出来:
Q1:SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; Q2:START TRANSACTION WITH CONSISTENT SNAPSHOT; /* other tables */ Q3:SAVEPOINT sp; /* 时刻 1 */ Q4:show create table `t1`; /* 时刻 2 */ Q5:SELECT * FROM `t1`; /* 时刻 3 */ Q6:ROLLBACK TO SAVEPOINT sp; /* 时刻 4 */ /* other tables */
这个事务模拟了进行导数据的整个事务过程:
在备份开始的时候,为了确保 RR(可重复读)隔离级别,再设置一次 RR 隔离级别 (Q1);
启动事务,这里用 WITH CONSISTENT SNAPSHOT 确保这个语句执行完就可以得到一个一致性视图(Q2);
设置一个保存点,这个很重要(Q3);
show create 是为了拿到表结构 (Q4),用SELECT * 模拟正式导数据 (Q5),回滚到 SAVEPOINT sp,在这里的作用是释放 t1 的 MDL 锁 (Q6)。
DDL 从主库传过来的时间按照效果不同,我打了四个时刻。题目设定为小表,我们假定到达后,如果开始执行,则很快能够执行完成。
参考答案如下:
其他table的DDL发生时刻:
如果在 时刻一Q4 语句执行之前到达,现象:没有影响,备份拿到的是 DDL 后的表结构。
如果在“时刻 2”到达,则表结构被改过,Q5 执行的时候,报 Table definition has changed, please retry transaction,现象:mysqldump 终止;
如果在“时刻 2”和“时刻 3”之间到达,mysqldump 占着 t1 的 MDL 读锁,另一张表的DDL阻塞,即binlog 阻塞。现象:主从数据结构之间存在延迟。原因: Q6 执行完成后其他tab拿到MDL锁执行DDL,此时从库已经备份完成,主库之后更新了DDL,。这部分信息没有同步到从库
从“时刻 4”开始,mysqldump 释放了 MDL 读锁,现象:没有影响,备份拿到的还是 DDL 前的表结构。
如果你要删除一个表里面的前 10000 行数据,有以下三种方法可以做到:
第一种,直接执行 delete from T limit 10000;
第二种,在一个连接中循环执行 20 次 delete from T limit 500;
第三种,在 20 个连接中同时执行 delete from T limit 500。
你会选择哪一种方法呢?为什么呢?
方案一,事务相对较长,则占用锁的时间较长,会导致其他客户端等待资源时间较长。
方案二,串行化执行,将相对长的事务分成多次相对短的事务,则每次事务占用锁的时间相对较短,其他客户端在等待相应资源的时间也较短。这样的操作,同时也意味着将资源分片使用(每次执行使用不同片段的资源),可以提高并发性。
方案三,人为自己制造锁竞争,加剧并发量。
(4.3) 问题3
在使用连接池的情况下,连接会复用.比如一个业务使用连接set sql_select_limit=1,释放掉以后.其他业务复用该连接时,这个参数也生效.请问怎么避免这种情况,或者怎么禁止业务set session?
reset_connection接口可以考虑一下:https://dev.mysql.com/doc/refman/5.7/en/lock-tables.html