【问题标题】:Working around MySQL error "Deadlock found when trying to get lock; try restarting transaction"解决 MySQL 错误“尝试获取锁定时发现死锁;尝试重新启动事务”
【发布时间】:2011-02-05 11:07:13
【问题描述】:

我有一个 MySQL 表,其中包含大约 5,000,000 行,这些行通过 DBI 连接的并行 Perl 进程以小方式不断更新。该表大约有 10 列和几个索引。

一个相当常见的操作有时会导致以下错误:

DBD::mysql::st execute failed: Deadlock found when trying to get lock; try restarting transaction at Db.pm line 276.

触发错误的SQL语句是这样的:

UPDATE file_table SET a_lock = 'process-1234' WHERE param1 = 'X' AND param2 = 'Y' AND param3 = 'Z' LIMIT 47

该错误仅在某些时候触发。我估计只有 1% 的电话或更少。但是,小表从未发生过这种情况,并且随着数据库的增长而变得越来越普遍。

请注意,我使用 file_table 中的 a_lock 字段来确保我正在运行的四个几乎相同的进程不会尝试在同一行上工作。限制旨在将他们的工作分成小块。

我没有对 MySQL 或 DBD::mysql 做太多调整。 MySQL是标准的Solaris部署,数据库连接设置如下:

my $dsn = "DBI:mysql:database=" . $DbConfig::database . ";host=${DbConfig::hostname};port=${DbConfig::port}";
my $dbh = DBI->connect($dsn, $DbConfig::username, $DbConfig::password, { RaiseError => 1, AutoCommit => 1 }) or die $DBI::errstr;

我在网上看到其他几个人报告了类似的错误,这可能是真正的死锁情况。

我有两个问题:

  1. 我的情况到底是什么导致了上述错误?

  2. 有没有一种简单的方法来解决它或降低它的频率?例如,“在 Db.pm 第 276 行重新启动事务”究竟该怎么做?

提前致谢。

【问题讨论】:

  • 要查看死锁的表以及导致死锁的一些数据,请运行SHOW ENGINE INNODB STATUS

标签: mysql deadlock database-deadlocks


【解决方案1】:

如果您使用 InnoDB 或任何行级事务 RDBMS,那么即使在完全正常的情况下,任何 写入事务都可能导致死锁。较大的表、较大的写入和较长的事务块通常会增加发生死锁的可能性。在你的情况下,它可能是这些的组合。

真正处理死锁的唯一方法是编写代码来预期它们。如果你的数据库代码写得很好,这通常不是很困难。通常,您可以在查询执行逻辑周围放置一个try/catch,并在发生错误时查找死锁。如果你抓住了一个,正常的做法就是再次尝试执行失败的查询。

我强烈建议您阅读 MySQL 手册中的this page。它列出了一系列有助于应对死锁和减少死锁频率的事情。

【讨论】:

  • 那么我们需要捕获哪些错误代码?单独抓1205就够了吗? dev.mysql.com/doc/refman/5.7/en/error-messages-server.html 中有超过 900 个错误代码。您如何知道我们需要捕获的所有代码才能为您的 try/catch 建议实施适当的解决方案?
  • 这是否意味着除了InnoDB or any row-level transactional RDBMS之外没有这些问题?
  • @Pacerier,我相信 1213 也是必须的——它比 1205 更频繁地发生在我身上。见mariadb.com/kb/en/mariadb-error-codes。也许 3058 也应该重试。
【解决方案2】:

答案是正确的,但是关于如何处理死锁的 perl 文档有点稀疏,并且可能与 PrintError、RaiseError 和 HandleError 选项混淆。似乎与其使用 HandleError,不如在 Print 和 Raise 上使用,然后使用 Try:Tiny 之类的东西来包装代码并检查错误。下面的代码给出了一个示例,其中 db 代码位于 while 循环内,该循环将每 3 秒重新执行一次错误的 sql 语句。 catch 块获取 $_ ,这是特定的错误消息。我将它传递给处理函数“dbi_err_handler”,该函数检查 $_ 是否有大量错误,如果代码应该继续(从而打破循环)则返回 1,如果它是死锁并且应该重试,则返回 0...

$sth = $dbh->prepare($strsql);
my $db_res=0;
while($db_res==0)
{
   $db_res=1;
   try{$sth->execute($param1,$param2);}
   catch
   {
       print "caught $_ in insertion to hd_item_upc for upc $upc\n";
       $db_res=dbi_err_handler($_); 
       if($db_res==0){sleep 3;}
   }
}

dbi_err_handler 至少应具有以下内容:

sub dbi_err_handler
{
    my($message) = @_;
    if($message=~ m/DBD::mysql::st execute failed: Deadlock found when trying to get lock; try restarting transaction/)
    {
       $caught=1;
       $retval=0; # we'll check this value and sleep/re-execute if necessary
    }
    return $retval;
}

您应该包括您希望处理的其他错误,并根据您是要重新执行还是继续设置 $retval..

希望这对某人有所帮助 -

【讨论】:

    【解决方案3】:

    请注意,如果您在插入之前使用SELECT FOR UPDATE 执行唯一性检查,除非您启用innodb_locks_unsafe_for_binlog 选项,否则每个竞争条件都会出现死锁。检查唯一性的无死锁方法是使用INSERT IGNORE将行盲目插入具有唯一索引的表中,然后检查受影响的行数。

    将下面的行添加到my.cnf 文件

    innodb_locks_unsafe_for_binlog = 1

    #

    1 - 开启
    0 - 关闭

    #

    【讨论】:

    • 这解决了我在多线程环境中保存 ActiveRecord 关联的所有问题。
    • 启用innodb_locks_unsafe_for_binlog 可能会导致幻像问题,因为在禁用间隙锁定时其他会话可以将新行插入到间隙中。
    【解决方案4】:

    在发生死锁异常的情况下重试查询的想法很好,但它可能非常慢,因为 mysql 查询将一直等待锁被释放。并且如果发生死锁,mysql会尝试查找是否存在死锁,即使在发现存在死锁后,它也会等待一段时间,然后再踢出一个线程以摆脱死锁情况。

    当我遇到这种情况时,我所做的是在你自己的代码中实现锁定,因为它是 mysql 的锁定机制由于错误而失败。所以我在我的 java 代码中实现了我自己的行级锁定:

    private HashMap<String, Object> rowIdToRowLockMap = new HashMap<String, Object>();
    private final Object hashmapLock = new Object();
    public void handleShortCode(Integer rowId)
    {
        Object lock = null;
        synchronized(hashmapLock)
        {
          lock = rowIdToRowLockMap.get(rowId);
          if (lock == null)
          {
              rowIdToRowLockMap.put(rowId, lock = new Object());
          }
        }
        synchronized (lock)
        {
            // Execute your queries on row by row id
        }
    }
    

    【讨论】:

    • 不幸的是,遇到这种情况的大多数用户很可能正在处理多台机器或将数据转储到单个 MySQL 实例的进程。应用程序中的行级锁定对于大多数用户来说根本不是一种选择。
    猜你喜欢
    • 1970-01-01
    • 2013-07-18
    • 1970-01-01
    • 2011-01-20
    • 2013-09-20
    • 1970-01-01
    • 2020-10-08
    • 2016-05-13
    相关资源
    最近更新 更多