在我的生产错误日志中,我偶尔会看到:

SQLSTATE[HY000]:一般错误:1205 超过锁等待超时;试一试 重新启动事务

我知道哪个查询在那个时刻试图访问数据库,但是是否有一种方法可以找出哪个查询在那个精确的时刻拥有锁?


当前回答

正如有人在关于这个问题的众多SO线程中的一个线程中提到的:有时已经锁定表的进程在进程列表中显示为休眠!我非常着急,直到我杀死了数据库中打开的所有睡眠线程(当时没有一个是活动的)。这最终解锁了表并让更新查询运行。

评论者说了类似于“有时MySQL线程锁定了一个表,然后在等待与MySQL无关的事情发生时进入睡眠状态。”

在重新检查了show engine innodb状态日志后(一旦我找到了负责锁的客户端),我注意到被卡住的线程被列在事务列表的最底部,在由于锁冻结而即将出错的活动查询下面:

------------------
---TRANSACTION 2744943820, ACTIVE 1154 sec(!!)
2 lock struct(s), heap size 376, 2 row lock(s), undo log entries 1
MySQL thread id 276558, OS thread handle 0x7f93762e7710, query id 59264109 [ip] [database] cleaning up
Trx read view will not see trx with id >= 2744943821, sees < 2744943821

(不确定“Trx读视图”消息是否与冻结锁相关,但与其他活动事务不同的是,这个事务不显示与发出的查询一起,而是声称事务正在“清理”,但有多个行锁)

这个故事的寓意是,即使线程处于睡眠状态,事务也可以是活动的。

其他回答

激活MySQL general.log(磁盘密集型)并使用mysql_analyse_general_log.pl来提取长时间运行的事务,例如:

——min-duration=你的innodb_lock_wait_timeout值

然后禁用general.log。

在记录中,锁等待超时异常也发生在出现死锁且MySQL无法检测到它时,因此它只是超时。另一个原因可能是一个运行时间非常长的查询,但是它更容易解决/修复,我在这里不描述这种情况。

如果死锁在两个事务中被“正确地”构造,MySQL通常能够处理死锁。然后MySQL只是杀死/回滚一个拥有较少锁的事务(不太重要,因为它影响的行较少),并让另一个事务完成。

现在,让我们假设有两个进程A和B和3个事务:

Process A Transaction 1: Locks X
Process B Transaction 2: Locks Y
Process A Transaction 3: Needs Y => Waits for Y
Process B Transaction 2: Needs X => Waits for X
Process A Transaction 1: Waits for Transaction 3 to finish

(see the last two paragraph below to specify the terms in more detail)

=> deadlock 

这是一个非常不幸的设置,因为MySQL无法看到有死锁(跨越3个事务)。MySQL做的是…没有什么!它只是等待,因为它不知道该做什么。它等待直到第一个获得的锁超过超时(进程A事务1:锁X),然后这将解锁锁X,从而解锁事务2等。

The art is to find out what (which query) causes the first lock (Lock X). You will be able to see easily (show engine innodb status) that Transaction 3 waits for Transaction 2, but you will not see which transaction Transaction 2 is waiting for (Transaction 1). MySQL will not print any locks or query associated with Transaction 1. The only hint will be that at the very bottom of the transaction list (of the show engine innodb status printout), you will see Transaction 1 apparently doing nothing (but in fact waiting for Transaction 3 to finish).

关于如何找到哪个SQL查询导致锁(lock X)被授予正在等待的给定事务的技术,在此处描述了在长时间运行的事务中跟踪MySQL查询历史

如果您想知道示例中的流程和事务究竟是什么。该进程是一个PHP进程。Transaction是由innodb-trx-table定义的事务。在我的例子中,我有两个PHP进程,在每个进程中我手动启动一个事务。有趣的是,即使我在一个进程中启动了一个事务,MySQL内部实际上使用了两个独立的事务(我不知道为什么,也许MySQL开发人员可以解释)。

MySQL is managing its own transactions internally and decided (in my case) to use two transactions to handle all the SQL requests coming from the PHP process (Process A). The statement that Transaction 1 is waiting for Transaction 3 to finish is an internal MySQL thing. MySQL "knew" the Transaction 1 and Transaction 3 were actually instantiated as part of one "transaction" request (from Process A). Now the whole "transaction" was blocked because Transaction 3 (a subpart of "transaction") was blocked. Because "transaction" was not able to finish the Transaction 1 (also a subpart of the "transaction") was marked as not finished as well. This is what I meant by "Transaction 1 waits for Transaction 3 to finish".

如果您正在使用JDBC,那么您可以选择 includeInnodbStatusInDeadlockExceptions = true

https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-configuration-properties.html

这个异常的最大问题是,它通常在测试环境中不可重现,当它发生在prod上时,我们无法运行innodb引擎状态。所以在其中一个项目中,我把下面的代码放入了这个异常的catch块中。这帮助我在异常发生时捕捉引擎状态。这帮了大忙。

Statement st = con.createStatement();
ResultSet rs =  st.executeQuery("SHOW ENGINE INNODB STATUS");
while(rs.next()){
    log.info(rs.getString(1));
    log.info(rs.getString(2));
    log.info(rs.getString(3));
}

争用越多,出现死锁的可能性就越大,DB引擎将通过对其中一个死锁事务进行超时处理来解决这个问题。

此外,已修改(例如UPDATE或DELETE)大量条目的长时间运行的事务更有可能与其他事务产生冲突。

虽然InnoDB MVCC,你仍然可以使用FOR UPDATE子句请求显式锁。然而,与其他流行的db (Oracle, MSSQL, PostgreSQL, DB2)不同,MySQL使用REPEATABLE_READ作为默认隔离级别。

现在,您获得的锁(通过修改行或使用显式锁定)将在当前运行的事务期间保持。如果你想了解REPEATABLE_READ和READ COMMITTED在锁方面的区别,请阅读这篇Percona文章。

在REPEATABLE READ中,事务期间获得的每个锁都被持有 在事务持续时间内。 在READ COMMITTED中,不匹配扫描的锁在STATEMENT完成后被释放。 ... 这意味着在READ COMMITTED中,其他事务可以自由地更新它们在update语句完成后无法更新的行(在REPEATABLE READ中)。

因此:隔离级别(REPEATABLE_READ, SERIALIZABLE)越严格,死锁的可能性就越大。这不是“本质上”的问题,而是一种权衡。

使用READ_COMMITTED可以获得非常好的结果,因为在使用跨越多个HTTP请求的逻辑事务时,需要防止应用程序级的丢失更新。乐观锁定方法针对即使使用SERIALIZABLE隔离级别也可能发生的丢失更新,同时通过允许使用READ_COMMITTED来减少锁争用。