2010-04-07 206 views
25

我有一个大约5,000,000行的MySQL表,它们通过并行Perl进程以小方式不断更新,这些并行Perl进程通过DBI连接。该表有大约10列和几个索引。解决MySQL错误“尝试获取锁定时发现死锁;尝试重新启动事务”

一个相当普遍的操作产生了有时以下错误:

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行的交易”?

在此先感谢。

回答

61

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

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

我强烈建议您在MySQL手册中阅读this page。它有一系列的事情可以帮助解决僵局并减少它们的频率。

+2

什么是我们需要捕捉,然后错误代码?仅靠1205就足够了吗? http://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html中有超过900个错误代码。您如何知道我们需要执行的所有代码,以便为您的try/catch建议实施适当的解决方案? – Pacerier 2014-12-19 03:52:12

+0

这是否意味着除了“InnoDB或任何行级事务性RDBMS”之外没有这些问题? – 2015-12-31 19:49:07

5

请注意,如果您使用SELECT FOR UPDATE插入之前执行唯一性检查,你将获得每一场比赛条件的僵局,除非你启用innodb_locks_unsafe_for_binlog选项。用于检查唯一性的无死锁方法是使用INSERT IGNORE盲目地将行插入具有唯一索引的表中,然后检查受影响的行数。

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

innodb_locks_unsafe_for_binlog = 1

1 - ON
0 - OFF

+0

这解决了在多线程环境中保存ActiveRecord关联的所有问题。 – lightyrs 2014-05-09 23:08:11

+2

启用'innodb_locks_unsafe_for_binlog'可能会导致幻影问题,因为禁用间隙锁定时,其他会话可以将新行插入到间隙中。 – shivam 2015-07-31 06:04:32

9

答案是正确的,但是Perl文档关于如何处理死锁有点稀疏,并可能与PrintError,RaiseError和HandleError混淆选项。看起来,与使用HandleError不同,在Print和Raise上使用,然后使用Try:Tiny来包装代码并检查错误。下面的代码给出了一个例子,其中db代码在一个while循环中,每3秒会重新执行一次错误的sql语句。 catch块获取$ _这是特定的err消息。我把它传递给一个处理函数“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 ..

希望这可以帮助别人 -

0

在死锁异常的情况下重试查询的想法是好的,但它可能会非常慢,因为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 
    } 
} 
+4

不幸的是,大多数遇到这种情况的用户可能会处理多台机器或将数据转储到单个MySQL实例中。对于大多数用户来说,应用程序中的行级锁定不是一种选择。 – dgtized 2015-03-12 19:48:53

相关问题