在 MySQL 中遇到死锁
Posted
技术标签:
【中文标题】在 MySQL 中遇到死锁【英文标题】:Getting deadlocks in MySQL 【发布时间】:2010-05-26 23:25:57 【问题描述】:我们非常沮丧地遇到了 mysql 中的死锁。这不是因为超过锁定超时,因为死锁在发生时会立即发生。下面是在 2 个独立线程(与连接池中的 2 个独立连接)上执行的 SQL 代码,它会产生死锁:
UPDATE Sequences SET Counter = LAST_INSERT_ID(Counter + 1) WHERE Sequence IS NULL
Sequences 表有 2 列:Sequence 和 Counter
LAST_INSERT_ID 允许我们retrieve this updated counter value as per MySQL's recommendation。这对我们来说是完美的,但是我们遇到了这些僵局!我们为什么会得到它们,我们如何避免它们??
非常感谢您对此的任何帮助。
编辑:这一切都在事务中(因为我使用的是 Hibernate,所以需要)并且 AUTO_INCREMENT 在这里没有意义。我应该更清楚。 Sequences 表包含许多序列(在我们的例子中大约有 1 亿个)。我需要增加一个计数器并检索该值。 AUTO_INCREMENT 在所有这些中都不起作用,这与 Id 或 PRIMARY KEY 无关。
【问题讨论】:
能否请您发布SHOW CREATE TABLE Sequences
的输出?
CREATE TABLE sequences
(Id
bigint(20) NOT NULL AUTO_INCREMENT, Sequence
varbinary(1005) DEFAULT NULL, Counter
int(11) DEFAULT '0', PRIMARY KEY ( Id
), 唯一键 Sequences_Constraint
(Sequence
(104)) ) ENGINE=InnoDB AUTO_INCREMENT=134 DEFAULT CHARSET=utf8
【参考方案1】:
将您的 sql 语句包装在事务中。如果您不使用事务,您将在 LAST_INSERT_ID 上获得竞争条件。
但实际上,你应该有计数器字段auto_increment,所以你让mysql来处理这个。
您的第三个解决方案是使用LOCK_TABLES 来锁定序列表,这样其他进程就不能同时访问它。除非您使用 INNODB,否则这可能是最慢的解决方案。
【讨论】:
这是一个事务。我在上面编辑了我的问题以解释 AUTO_INCREMENT 在这里不相关,抱歉不清楚。锁定 Sequences 表对我们来说可能是一个严重的性能问题。 事务导致更多个死锁......它们不是解决死锁的解决方案。【参考方案2】:死锁是任何事务数据库的正常部分,并且随时可能发生。通常,您应该编写应用程序代码来处理它们,因为没有万无一失的方法来保证您永远不会遇到死锁。话虽如此,有些情况会增加发生死锁的可能性,例如使用大型事务,您可以采取一些措施来减轻它们的发生。
首先,您应该阅读this manual page 以更好地了解如何避免它们。
其次,如果您所做的只是更新计数器,那么您应该真的、真的、真的使用 Counter
的 AUTO_INCREMENT 列,而不是依赖“选择然后更新”过程,正如您所看到的那样可能产生死锁的竞争条件。本质上,表列的 AUTO_INCREMENT 属性将充当您的计数器。
最后,我将假设您在事务中有更新语句,因为这会产生频繁的死锁。如果您想看到它的实际效果,请尝试experiment listed here。这正是您的代码所发生的事情......两个线程试图在其中一个被提交之前同时更新相同的记录。瞬间死锁。
您最好的解决方案是弄清楚如何在没有事务的情况下执行此操作,而 AUTO_INCREMENT 会让您这样做。
【讨论】:
链接到有趣的实验,谢谢。但我不确定这就是这里发生的事情。我只更新 1 行。我会阅读手册页。这当然是在交易中。我在上面编辑了我的问题以解释 AUTO_INCREMENT 在这里不相关,抱歉不清楚。 @at - 您只更新一行,但在两个事务中它是同一行,因此出现死锁。如果两个事务同时命中,则两者都将尝试更新同一行,因为LAST_INSERT_ID()
在一个可以提交之前对于每个事务都是相同的。
为什么更新同一行的语句不能一个接一个地执行?为什么会出现僵局?【参考方案3】:
不涉及其他 SQL 吗?对我来说似乎有点不太可能。
“where sequence is null”可能会导致全表扫描,从而导致在每一行/页/...上获取读取锁。
如果(您的特定引擎不使用 MVCC 并且)在同一事务中的更新之前有一个 INSERT,这将成为一个问题。该 INSERT 将在某些资源(行/页/...)上获得排他锁,这将导致任何其他线程获取读锁等待。因此,两个连接可以首先进行插入,导致它们每个都在表的一小部分上拥有排他锁,然后它们都尝试进行更新,要求它们每个都能够在表上获取读锁整张桌子。
【讨论】:
死锁中没有涉及与该表有任何关系的其他 SQL。死锁都发生在上面的 UPDATE 语句上。您对 NULL 问题的看法很有趣。还有一个 UPDATE 语句,其中序列名称有一个值,但死锁不会在那里发生。我认为由于我们应用程序的性质,我们需要空序列的频率要高得多。如果 NULL 导致全表扫描,我可以轻松更改它以查找其他内容,例如空字符串。我在同一个事务中确实有插入,这会导致死锁吗? “没有涉及其他 SQL”和“我在同一个事务中有插入”???来吧。死锁发生在事务之间,而不是语句级别。为什么你首先要写“WHERE ... IS NULL”?您不想将更新限制在同一事务刚刚插入的行吗? 您问我是否没有涉及其他 SQL,然后建议问题可能是同一事务中的插入语句。这就是为什么我用那些引用的短语来回应。 “WHERE Sequence IS NULL”实际上将我的更新限制在 Sequence 为空的行。我对你的最后一个问题有点困惑。插入只是因为序列名称在 Sequences 表中尚不存在。【参考方案4】:我设法使用序列的 MyISAM 表来做到这一点。
然后我有一个名为 getNextCounter 的函数,它执行以下操作:
执行 SELECT sequence_value FROM 序列 where sequence_name = 'test'; 执行更新:更新序列 SET sequence_value = LAST_INSERT_ID(last_retrieved_value + 1) WHERE sequence_name = 'test' and sequence_value = last retrieved value; 循环重复,直到两个查询都成功,然后检索最后一个插入 id。由于它是一个 MyISAM 表,它不会成为您事务的一部分,因此该操作不会导致任何死锁。
【讨论】:
以上是关于在 MySQL 中遇到死锁的主要内容,如果未能解决你的问题,请参考以下文章