【问题标题】:How to avoid MySQL 'Deadlock found when trying to get lock; try restarting transaction'如何避免 MySQL '尝试获取锁时发现死锁;尝试重新启动事务'
【发布时间】:2011-01-20 22:12:05
【问题描述】:

我有一个记录在线用户的 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

然后它会进行一些计数以记录一些统计信息(即:在线成员、在线访问者)。

【问题讨论】:

  • 你能提供更多关于表结构的细节吗?是否有任何聚集或非聚集索引?
  • dev.mysql.com/doc/refman/5.1/en/innodb-deadlocks.html - 运行“show engine innodb status”将提供有用的诊断。
  • 插入单行如何导致该插入的死锁?我期待在事务中,您需要尝试获取至少两个锁才能死锁。插入一行只持有一个锁。我认为您在第一个查询中缺少一些插入或选择。

标签: mysql deadlock


【解决方案1】:

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

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

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

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

【讨论】:

  • 所以也许我的问题是用户刷新了页面,因此在 cron 尝试在记录上运行 DELETE 的同时触发了记录的更新。但是,我在 INSERTS 上遇到错误,因此 cron 不会删除刚刚创建的记录。那么如何在尚未插入的记录上发生死锁呢?
  • 您能否提供更多有关表的信息以及事务的具体用途?
  • 如果每个事务只有一个语句,我看不出死锁是如何发生的。其他表没有其他操作?没有特殊的外键或唯一约束?没有级联删除约束?
  • 不,没有什么特别的……我想这取决于桌子的使用性质。访问者的每次页面刷新都会插入/更新一行。任何时候都有大约 1000 多名访问者访问。
【解决方案2】:

删除语句很可能会影响表中总行数的很大一部分。最终这可能会导致在删除时获取表锁。持有一个锁(在这种情况下是行锁或页锁)并获得更多锁总是存在死锁风险。但是我无法解释为什么 insert 语句会导致锁升级——它可能与页面拆分/添加有关,但是更了解 MySQL 的人必须在那里填写。

首先,值得尝试立即为删除语句显式获取表锁。请参阅LOCK TABLESTable locking issues

【讨论】:

    【解决方案3】:

    一个有助于解决大多数死锁的简单技巧是按特定顺序对操作进行排序。

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

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

    如果两者同时运行,连接 1 将锁定 key(1),连接 2 将锁定 key(2),每个连接都将等待对方释放 key -> 死锁。

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

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

    不可能出现死锁。

    这就是我的建议:

    1. 确保您没有其他查询会一次锁定多个键的访问权限,但删除语句除外。如果你这样做(我怀疑你这样做),请按升序排列它们在 (k1,k2,..kn) 中的 WHERE。

    2. 修正你的删除语句以升序工作:

    改变

    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 次)。

    【讨论】:

    • 如果您启用了交易,那就是全部或全部。如果您有任何类型的异常,则可以保证整个交易无效。在那种情况下,你会想要重新开始整个事情。
    • 基于大表选择的删除比简单的删除要慢得多
    • 非常感谢,伙计。 “排序语句”提示修复了我的死锁问题。
    • @OmryYadan 据我所知,在 MySQL 中,您无法从进行 UPDATE 的同一个表中选择子查询。 dev.mysql.com/doc/refman/5.7/en/update.html
    • 删除查询中的项目如何排序修复死锁?
    【解决方案4】:

    您可以尝试让 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

    【讨论】:

      【解决方案5】:

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

      更多信息请参见@RetryTransactionJavadoc。

      【讨论】:

        【解决方案6】:

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

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

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

        当我删除 MySqlTransaction 时,我能够毫无问题地并行运行该方法。

        只是分享我的经验,我不提倡任何东西。

        【讨论】:

          【解决方案7】:

          如果有人仍在为这个问题苦苦挣扎:

          我遇到了类似的问题,即 2 个请求同时访问服务器。没有出现以下情况:

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

          所以,我很困惑为什么会发生死锁。

          然后我发现因为外键,两个表之间存在父子关系。当我在子表中插入记录时,事务正在获取父表行的锁定。紧接着,我试图将触发锁定提升的父行更新为独占行。由于第二个并发事务已经持有共享锁,因此导致死锁。

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

          【讨论】:

          • 在我的情况下,看起来问题出在外键关系上。谢谢1
          • 对我来说也一样:更新了一个表,更新了另一个表中的外键。我用无操作删除了我的约束键。然后,奇迹,没有更多的僵局!当然,它不再检查 contrainst,但它不适用于大型更新表。非常感谢!
          【解决方案8】:

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

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

          另外,INDEX(datetime) 对于避免死锁非常重要。

          但是,如果日期时间测试包括超过 20% 的表格,DELETE 将执行表格扫描。更频繁地删除较小的块是一种解决方法。

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

          底线:

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

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

          【讨论】:

            【解决方案9】:

            @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

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2013-07-18
              • 2020-03-27
              • 2013-09-20
              • 2011-02-05
              • 1970-01-01
              • 2020-10-08
              相关资源
              最近更新 更多