web-dev-qa-db-ja.com

MySQLエラー「ロックを取得しようとしたときにデッドロックが見つかりました。トランザクションを再起動してください」の回避策

約5,000,000行のMySQLテーブルがあり、DBIを介して接続する並列Perlプロセスによって小さな方法で絶えず更新されています。テーブルには約10列といくつかのインデックスがあります。

かなり一般的な操作の1つでは、次のエラーが発生することがあります。

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フィールドを使用して、実行している4つのほぼ同一のプロセスが同じ行で試行および動作しないようにしていることに注意してください。制限は、作業を小さなチャンクに分割するように設計されています。

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;

他の何人かが同様のエラーを報告しており、これが真のデッドロック状況である可能性があることをオンラインで見ました。

2つの質問があります。

  1. 上記のエラーを引き起こしている私の状況はどうですか?

  2. それを回避したり、頻度を減らしたりする簡単な方法はありますか?たとえば、「Db.pm行276でトランザクションを再開する」ことをどのように正確に実行できますか?

前もって感謝します。

28
Anon Gordon

InnoDBまたは行レベルのトランザクションRDBMSを使用している場合、anywriteトランザクションは、完全に通常の状況でもデッドロックを引き起こす可能性があります。テーブルが大きくなり、書き込みが大きくなり、トランザクションブロックが長くなると、デッドロックが発生する可能性が高くなります。あなたの状況では、おそらくこれらの組み合わせです。

デッドロックを本当に処理する唯一の方法は、デッドロックを予測するコードを書くことです。データベースコードが適切に記述されていれば、これは一般的にそれほど難しくありません。多くの場合、単にtry/catchクエリ実行ロジックを囲み、エラーが発生したときにデッドロックを探します。キャッチした場合、通常行うべきことは、失敗したクエリを再度実行しようとすることです。

MySQLマニュアルの このページ を読むことを強くお勧めします。デッドロックに対処し、その頻度を減らすのに役立つことのリストがあります。

71
zombat

答えは正しいですが、デッドロックの処理方法に関するPerlのドキュメントは少しまばらで、おそらくPrintError、RaiseError、HandleErrorオプションと混同します。 HandleErrorではなく、Print and Raiseで使用してから、Try:Tinyなどを使用してコードをラップし、エラーをチェックしているようです。以下のコードは、dbコードがwhileループ内にあり、エラーが発生したsqlステートメントを3秒ごとに再実行する例を示しています。 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を設定する必要があります。

これが誰かを助けることを願っています-

10
Ross

挿入前にSELECT FOR UPDATEを使用して一意性チェックを実行すると、innodb_locks_unsafe_for_binlogオプションを有効にしない限り、すべての競合状態でデッドロックが発生することに注意してください。一意性を確認するデッドロックのない方法は、INSERT IGNOREを使用して一意のインデックスを持つテーブルに行を盲目的に挿入し、影響を受ける行数を確認することです。

my.cnfファイルに以下の行を追加します

innodb_locks_unsafe_for_binlog = 1

1-オン
0-オフ

7

デッドロック例外の場合にクエリを再試行するというアイデアは良いですが、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
    }
}
0
user517491