35

我有一个 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 行重新启动事务”?

提前致谢。

4

4 回答 4

78

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

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

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

于 2010-04-07T21:33:26.153 回答
13

答案是正确的,但是关于如何处理死锁的 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 ..

希望这可以帮助某人-

于 2012-02-16T17:01:48.060 回答
8

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

将以下行添加到my.cnf文件

innodb_locks_unsafe_for_binlog = 1

#

1 - 开
0 - 关

#
于 2012-02-16T14:50:11.787 回答
1

在死锁异常的情况下重试查询的想法很好,但它可能非常慢,因为 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
    }
}
于 2013-03-30T07:34:38.530 回答