如何避免mysql'试图获取锁时发现死锁;尝试重新开始交易”


286

我有一个记录在线用户的innoDB表。用户每次刷新页面时都会更新该信息,以跟踪他们所访问的页面以及它们对该站点的最后访问日期。然后,我会有一个cron,每15分钟运行一次,以删除旧记录。

我在尝试获取锁时发现“死锁;昨晚尝试重新启动事务”大约5分钟,这似乎是在向该表中运行INSERT时出现的情况。有人可以建议如何避免此错误吗?

===编辑===

以下是正在运行的查询:

首次访问网站:

INSERT INTO onlineusers SET
ip = 123.456.789.123,
datetime = now(),
userid = 321,
page = '/thispage',
area = 'thisarea',
type = 3

在每个页面上刷新:

UPDATE onlineusers SET
ips = 123.456.789.123,
datetime = now(),
userid = 321,
page = '/thispage',
area = 'thisarea',
type = 3
WHERE id = 888

每15分钟Cron:

DELETE FROM onlineusers WHERE datetime <= now() - INTERVAL 900 SECOND

然后,它会做一些计数来记录某些统计信息(即:在线成员,在线访客)。


您能否提供有关表结构的更多详细信息?是否有任何聚集索引或非聚集索引?
安德斯·亚伯

13
dev.mysql.com/doc/refman/5.1/en/innodb-deadlocks.html-运行“显示引擎innodb状态”将提供有用的诊断信息。
马丁2010年

当用户踏上页面时,执行同步数据库写入不是一个好习惯。正确的方法是将其保留在内存中,例如内存缓存或一些快速队列,然后使用cron写入db。
Nir

Answers:


291

一个可以解决大多数僵局的简单技巧就是按特定顺序对操作进行排序。

当两个事务试图以相反的顺序锁定两个锁时,会出现死锁,即:

  • 连接1:锁定键(1),锁定键(2);
  • 连接2:锁定键(2),锁定键(1);

如果两个都同时运行,则连接1将锁定键(1),连接2将锁定键(2),每个连接将等待另一个释放键->死锁。

现在,如果您更改查询以使连接按相同顺序锁定键,即:

  • 连接1:锁定键(1),锁定键(2);
  • 连接2:锁键(1),锁键(2);

陷入僵局是不可能的。

所以这是我的建议:

  1. 确保除了delete语句外,没有其他一次锁定访问多个键的查询。如果您这样做(我怀疑您这样做),请按升序在(k1,k2,.. kn)中订购他们的WHERE。

  2. 修复您的delete语句以升序工作:

更改

DELETE FROM onlineusers WHERE datetime <= now() - INTERVAL 900 SECOND

DELETE FROM onlineusers WHERE id IN (SELECT id FROM onlineusers
    WHERE datetime <= now() - INTERVAL 900 SECOND order by id) u;

要记住的另一件事是,mysql文档建议在发生死锁的情况下,客户端应自动重试。您可以将此逻辑添加到客户代码中。(说,在放弃之前,3个重试此特定错误)。


2
如果我有Transaction(autocommit = false),则引发死锁异常。仅重试同一条语句就足够了。executeUpdate()或现在已对整个事务进行了限制,应该回滚并重新运行其中正在运行的所有内容?
Whome

5
如果您启用了交易,则全部或全部不进行交易。如果您有任何例外情况,则可以保证整个交易均无效。在这种情况下,您将需要重新启动整个过程。
Omry Yadan 2014年

4
基于巨大表上的选择进行的删除比简单删除要慢得多
Thermech 2014年

3
非常感谢你,老兄 “排序声明”提示解决了我的死锁问题。
Miere 2015年

4
@OmryYadan据我所知,在MySQL中,您无法在进行UPDATE的同一表中选择子查询。dev.mysql.com/doc/refman/5.7/en/update.html
artaxerxe

72

当两个事务相互等待以获取锁时,就会发生死锁。例:

  • Tx 1:先锁定A,然后锁定B
  • Tx 2:锁定B,然后锁定A

关于死锁有很多问题和答案。每次您插入/更新/或删除行时,都会获得一个锁。为了避免死锁,您必须确保并发事务不会按可能导致死锁的顺序更新行。一般来说,即使在不同的事务中,也要尝试始终以相同的顺序获取锁(例如,始终先获取表A,然后再获取表B)。

数据库死锁的另一个原因可能是缺少索引。当插入/更新/删除一行时,数据库需要检查关系约束,即确保关系一致。为此,数据库需要检查相关表中的外键。这可能会导致获取除已修改的行以外的其他锁。然后确保始终在外键(当然还有主键)上具有索引,否则可能导致表锁而不是行锁。如果发生表锁定,则锁定争用会更高,并且出现死锁的可能性也会增加。


3
因此,也许我的问题是用户刷新了页面,从而在cron尝试对记录运行DELETE的同时触发了记录的更新。但是,由于在插入时出现错误,因此cron不会删除刚刚创建的记录。那么,如何在尚未插入的记录上发生死锁呢?
David

您能否提供更多有关该表以及事务处理的信息?
ewernli'2

如果每个事务只有一个语句,我看不到如何发生死锁。在其他表上没有其他操作?没有特殊的外键或唯一约束?没有级联删除约束?
ewernli 2010年

不,没有其他特别的...我认为这取决于表的使用性质。访问者每次刷新页面都会插入/更新一行。随时有大约1000多名访客在参观。
戴维

12

delete语句可能会影响表中总行的很大一部分。最终,这可能导致删除时获取表锁。保持锁(在这种情况下为行锁或页锁)并获取更多锁始终会带来死锁风险。但是我无法解释为什么insert语句会导致锁升级-它可能与页面拆分/添加有关,但是知道MySQL的人一定会在其中进行填充。

首先,值得尝试立即为delete语句显式获取表锁。请参阅“ 锁定表”和“ 表锁定问题”


6

您可以尝试delete通过首先将要删除的每一行的键插入临时表(如此伪代码)中来使该作业起作用

create temporary table deletetemp (userid int);

insert into deletetemp (userid)
  select userid from onlineusers where datetime <= now - interval 900 second;

delete from onlineusers where userid in (select userid from deletetemp);

像这样破坏它效率不高,但是避免了在期间按住键范围锁的需求delete

另外,修改select查询以添加一个where子句,该子句不包括超过900秒的行。这避免了对cron作业的依赖,并允许您重新安排其运行频率。

关于死锁的理论:我在MySQL中没有太多的背景知识,但是这里... delete它将持有日期时间的键范围锁,以防止where在事务中间添加匹配其子句的行,并且当它找到要删除的行时,它将尝试在要修改的每个页面上获得一个锁。会在insert要插入的页面上获取一个锁,然后尝试获取键锁。通常,insert将耐心等待该键锁打开,但是如果delete尝试锁定insert正在使用的同一页面,则会死锁,因为delete需要该页面锁和该insert键锁。这似乎并不适合插入虽然,deleteinsert 使用的日期时间范围不重叠,因此可能正在发生其他情况。

http://dev.mysql.com/doc/refman/5.1/en/innodb-next-key-locking.html


4

如果有人仍在努力解决此问题:

我遇到了类似的问题,其中2个请求同时到达服务器。没有如下情况:

T1:
    BEGIN TRANSACTION
    INSERT TABLE A
    INSERT TABLE B
    END TRANSACTION

T2:
    BEGIN TRANSACTION
    INSERT TABLE B
    INSERT TABLE A
    END TRANSACTION

所以,我很困惑为什么会发生僵局。

然后我发现由于外键,两张桌子之间有亲子关系。当我在子表中插入一条记录时,该事务正在获得对父表行的锁定。此后,我立即尝试更新父行,这将触发将锁提升为EXCLUSIVE 1。由于第二个并发事务已经持有SHARED锁,因此导致了死锁。

请参阅:https : //blog.tekenlight.com/2019/02/21/database-deadlock-mysql.html


在我看来,问题似乎也是外键关系。Thanks1
克里斯王子,

3

对于使用Spring的Java程序员,我通过使用AOP方面避免了这个问题,该方面会自动重试遇到瞬时死锁的事务。

有关更多信息,请参见@RetryTransaction Javadoc。


0

我有一个方法,其内部包装在MySqlTransaction中。

当我与自身并行运行相同方法时,出现了死锁问题。

运行该方法的单个实例没有问题。

当我删除MySqlTransaction时,我能够与自身并行运行该方法而没有任何问题。

只是分享我的经验,我什么也不提倡。


0

cron是危险的。如果一个cron实例在下一个实例到期之前未能完成,则它们很可能会互相打架。

最好有一个连续运行的作业,该作业将删除一些行,休眠一些行,然后重复执行。

同样,INDEX(datetime)对于避免死锁也非常重要。

但是,如果datetime测试包含超过表的20%,则 DELETE则将进行表扫描。解决方法是,删除较小的块,并更频繁地删除。

使用较小的块的另一个原因是锁定更少的行。

底线:

  • INDEX(datetime)
  • 持续运行的任务-删除,睡眠一分钟然后重复。
  • 为确保上述任务没有终止,请执行cron作业,其唯一目的是在失败时重新启动它。

其他删除技术:http : //mysql.rjweb.org/doc.php/deletebig

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.