我有一个记录在线用户的innoDB表。它会在用户每次刷新页面时更新,以跟踪他们所在的页面以及他们上次访问网站的日期。然后我有一个每15分钟运行一次的cron来删除旧记录。
我找到了'试图锁定时发现的死锁;尝试重新启动事务'昨晚约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
Cron每15分钟一次:
DELETE FROM onlineusers WHERE datetime <= now() - INTERVAL 900 SECOND
然后记录一些统计数据(即:在线成员,在线访客)。
答案 0 :(得分:251)
一个可以帮助解决大多数死锁的简单技巧是按特定顺序对操作进行排序。
当两个事务试图以相反的顺序锁定两个锁时,你会遇到死锁,即:
如果两者同时运行,则连接1将锁定密钥(1),连接2将锁定密钥(2),并且每个连接将等待另一个连接释放密钥 - &gt;死锁。
现在,如果您更改了查询,连接将以相同的顺序锁定密钥,即:
不可能陷入僵局。
所以这就是我的建议:
确保除了delete语句之外,没有其他查询一次锁定多个密钥。如果你这样做(我怀疑你这样做),请按升序排列他们的WHERE(k1,k2,.. kn)。
修复您的删除语句按升序运行:
更改
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次重试)。
答案 1 :(得分:64)
当两个事务相互等待获取锁定时发生死锁。示例:
有很多关于死锁的问题和答案。每次插入/更新/删除行时,都会获取锁定。为避免死锁,您必须确保并发事务不会按顺序更新行,从而导致死锁。一般来说,即使在不同的交易中,也会尝试以相同的顺序获取锁定(例如,首先是表A,然后是表B)。
数据库死锁的另一个原因可能是缺少索引。插入/更新/删除行时,数据库需要检查关系约束,即确保关系一致。为此,数据库需要检查相关表中的外键。 可能导致获取其他锁定而不是修改的行。确保总是在外键(当然还有主键)上有索引,否则可能导致表锁而不是行锁。如果发生表锁定,则锁争用会更高,并且死锁的可能性会增加。
答案 2 :(得分:9)
delete语句很可能会影响表中总行数的很大一部分。最终,这可能会导致在删除时获取表锁。持有锁(在这种情况下是行锁或页锁)并获得更多锁定始终是一种死锁风险。但是我无法解释为什么insert语句会导致锁定升级 - 它可能与页面拆分/添加有关,但是更好地了解MySQL的人必须在那里填写。
首先,可以尝试立即为delete语句显式获取表锁。请参阅LOCK TABLES和Table locking issues。
答案 3 :(得分: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
将持有日期时间的键范围锁,以防止与{{1}匹配的行1}}子句来自在事务中间添加,并且当它找到要删除的行时,它将尝试在它正在修改的每个页面上获取锁。 where
将在其插入的页面上获取锁定,然后尝试获取密钥锁定。通常情况下,insert
会耐心等待该密钥锁打开,但如果insert
尝试锁定delete
正在使用的同一页面,这将导致死锁,因为insert
需要该页面锁定和delete
需要该键锁定。这似乎不适合插入,insert
和delete
正在使用不重叠的日期时间范围,因此可能正在进行其他操作。
http://dev.mysql.com/doc/refman/5.1/en/innodb-next-key-locking.html
答案 4 :(得分:2)
对于使用Spring的Java程序员,我使用AOP方面避免了这个问题,该方面会自动重试遇到瞬态死锁的事务。
有关详细信息,请参阅@RetryTransaction Javadoc。
答案 5 :(得分:1)
万一有人还在为这个问题苦苦挣扎:
我遇到了类似的问题,其中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
答案 6 :(得分:0)
我有一个方法,其内部包装在MySqlTransaction中。
当我同时运行相同的方法时,出现了死锁问题。
运行该方法的单个实例没有问题。
当我删除MySqlTransaction时,我能够与自身并行运行该方法,而没有任何问题。
只分享我的经验,我什么也不提。
答案 7 :(得分:0)
cron
很危险。如果一个cron实例在下一个实例到期之前未能完成,则它们很可能会互相打架。
最好有一个连续运行的作业,该作业将删除一些行,休眠一些行,然后重复执行。
此外,INDEX(datetime)
对于避免死锁也非常重要。
但是,如果日期时间测试包含超过20%的表,则DELETE
将进行表扫描。解决方案:较小的块通常被删除,这是一种变通方法。
使用较小的块的另一个原因是锁定更少的行。
底线:
INDEX(datetime)
答案 8 :(得分:0)
@Omry Yadan 的回答 ( https://stackoverflow.com/a/2423921/1810962 ) 可以通过使用 ORDER BY 来简化。
改变
DELETE FROM onlineusers
WHERE datetime <= now() - INTERVAL 900 SECOND
到
DELETE FROM onlineusers
WHERE datetime <= now() - INTERVAL 900 SECOND
ORDER BY ID
保持您删除项目的顺序一致。此外,如果您在单个事务中进行多次插入,请确保它们也始终按 id 排序。
根据mysql删除文档:
<块引用>如果指定了 ORDER BY 子句,则按照指定的顺序删除行。
您可以在此处找到参考:https://dev.mysql.com/doc/refman/8.0/en/delete.html