在这种情况下如何避免死锁?

Posted

技术标签:

【中文标题】在这种情况下如何避免死锁?【英文标题】:How to avoid Deadlock in this scenario? 【发布时间】:2015-04-14 02:31:17 【问题描述】:

我有一个 innoDB 表,其中多个连接可能会插入数据,并且每 10 秒运行一次的单个 mysql 事件会删除一些以前插入的记录。

但我遇到了死锁。我怎样才能避免它们?

这段代码负责将记录逐行插入到表中。

sql = "INSERT INTO ex_result (Result_ID, ParentResult_ID, StepNumber, Name, Type, DB_ID, Session_ID, Status, TotalTime, FunctionCall, FunctionResult, ToolTime, PluginName, Screenshot_FID, IsNegative, ContinueOnError, WantSnapshot, Message, ResultCode, Output, InputArguments) VALUES (@Result_ID, @ParentResult_ID, @StepNumber, @Name, @Type, @DB_ID, @Session_ID, @Status, @TotalTime, @FunctionCall, @FunctionResult, @ToolTime, @PluginName, @Screenshot_FID, @IsNegative, @ContinueOnError, @WantSnapshot, @Message, @ResultCode, @Output, @InputArguments)"

事件的代码是:

DELIMITER //

CREATE EVENT EVENT_RESULT_CLEANER
ON SCHEDULE
EVERY 10 second
COMMENT 'Clean all delted results'
DO
BEGIN

DROP TEMPORARY TABLE IF EXISTS `Temp_Result_Purge`;

CREATE TEMPORARY TABLE `Temp_Result_Purge` (
`Result_ID` VARCHAR(63) NOT NULL,
PRIMARY KEY (`Result_ID`))
ENGINE = MEMORY;

INSERT INTO Temp_Result_Purge(Result_ID)
(
    SELECT t1.result_id
    FROM ex_result AS t1
    INNER JOIN ex_session as t2
    ON t1.session_id=t2.session_id
    WHERE NOT EXISTS(SELECT t3.result_id FROM ex_result as t3 WHERE t3.parentresult_id=t1.result_id)
    AND t2.DeletedOn IS NOT NULL
    LIMIT 2000
);

DELETE t1 FROM `ex_result` AS t1 INNER JOIN
`Temp_Result_Purge` AS t2 ON t1.Result_ID=t2.Result_ID;

DROP TEMPORARY TABLE `Temp_Result_Purge`;

END//

DELIMITER ;

【问题讨论】:

我不明白为什么你创建一个临时表 'Temp_Result_Purge' 而不是直接 DELETE t1 FROM ex_result AS t1 INNER JOIN ex_session as t2 ON t1.session_id=t2.session_id WHERE NOT EXISTS(SELECT t3 .result_id FROM ex_result as t3 WHERE t3.parentresult_id=t1.result_id) AND t2.DeletedOn IS NOT NULL 你确定有死锁吗?很奇怪,单个 INSERT 是如何导致死锁的。有没有办法同时多次运行删除程序?这可能是死锁的原因。 请提供 SHOW CREATE TABLE ex_result。 在遇到死锁后立即执行 SHOW ENGINE INNODB STATUS 并向我们提供陷入死锁的两条语句。 @RickJames:在问题link 中添加了相同的 CreateTable 和 InnoDBStatus 【参考方案1】:

首先我有一些讨厌的事情要说,然后我会找到一个可能的解决方案。

“不要排队,就去做吧。” -- MySQL 不是一个好的队列引擎。

添加 BEGIN...COMMIT(如前所述)。 BEGIN...COMMIT 也需要围绕其他代码。

添加代码来测试死锁。然后重播 BEGIN...COMMIT。你无法避免所有死锁,所以要为它们做好计划。

将 LIMIT 降到仅 10。然后将净化器置于连续循环中,而不是每 10 秒唤醒一次。 (如果您要说“这会让事情变得更糟”,请继续阅读;我会给您一个可能会更好的变体。)

使用LEFT JOIN ... WHERE ... IS NULL 代替NOT EXISTS ( SELECT ... )

不要一遍又一遍地重新创建表格;只是TRUNCATE TABLE。或者,更好的是,直接删除而不通过 tmp 表。然而,这就引出了另一个问题……

查询必须经过多少行才能找到 LIMIT 行?请记住,SELECT 会干扰 INSERT。如果它通常必须扫描一百万行才能找到 2000 行要删除,那么我们需要想办法让这些行更容易找到。为此,我们需要有关您的应用程序和表格大小的更多详细信息。或者想想这个……

礼貌地扫描一百万行以找到其中几行的一种技术是一次遍历表 1000 行,通常使用主键。注意:这是表的 1000 行,而不是符合删除条件的 1000 行。在每 1000 个之后,删除你找到的那些(如果有的话),然后继续下一个 1000 个。当你到达表格的末尾时,重新开始。有关如何执行此分块的详细信息,请参见此处:http://mysql.rjweb.org/doc.php/deletebig#deleting_in_chunks

【讨论】:

一次分块应该在 100 到 1000 行的范围内。您的 5000 批将太长,尤其是在添加 BEGIN...COMMIT 之后。仍然分块会使事情运行速度提高 10 倍,从而大大降低死锁的频率。【参考方案2】:

至少,你需要在你的事件代码中启动一个事务。

这不是在开始...结束时完成的,请参阅http://dev.mysql.com/doc/refman/5.6/en/commit.html

在所有存储程序(存储过程和函数, 触发器和事件),解析器将 BEGIN [WORK] 视为 BEGIN ... END 块的开头。在此开始交易 使用 START TRANSACTION 代替上下文。

此外,如果您每 10 秒运行一次,请考虑更改您的架构。关系数据库不适合生命周期短的数据。另一种可能是消息队列。

【讨论】:

我会尝试在 Event 中实现 Transaction 并观察。它应该会有所帮助。【参考方案3】:

我认为最好的解决方案是使用soft deletes

只需将已删除对象的状态设置为已删除即可。如果记录量真的很大,并且您根本不想存储历史数据,您可以每晚或每周定期清除数据库

其中一个缺点是您必须通过添加一个新条件来重写数据检索逻辑

【讨论】:

我们已经在 ex_session 表中实现了软删除(参见 t2.DeletedOn IS NULL)。清除由事件完成,但 INSERT 可能随时与清除事件发生冲突。

以上是关于在这种情况下如何避免死锁?的主要内容,如果未能解决你的问题,请参考以下文章

如何避免死锁

多线程死锁避免

在 SELECT ... INNER JOIN ... FOR UPDATE 的情况下,字符串的顺序是啥锁定以及如何避免死锁?

如何避免死锁

java如何避免死锁

事务 - 如何避免死锁?