web-dev-qa-db-ja.com

2つの更新によりデッドロックが発生する

私は次のスキーマを持つmysqlキューを持っています。

CREATE TABLE IF NOT EXISTS `media` (
  `mid` int(11) NOT NULL AUTO_INCREMENT,
  `order_type` int(11) NOT NULL,
  `media_id` varchar(50) COLLATE utf8_unicode_ci NOT NULL,
  `package` int(11) NOT NULL,
  `sent` int(11) NOT NULL,
  `timestamp` datetime NOT NULL,
  `t_check` int(11) NOT NULL,
  `performed_by` varchar(20) COLLATE utf8_unicode_ci DEFAULT NULL,
  `last_run` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `flag` int(11) NOT NULL,
  `package_sent_diff` int(11) AS (package-sent) PERSISTENT,
  PRIMARY KEY (`mid`),
  KEY `last_run` (`last_run`),
  KEY `performed_by` (`performed_by`),
  KEY `sent` (`sent`),
  KEY `package` (`package`),
  KEY `sent_package` (`sent`,`package`),
  KEY `package_sent_diff` (`package_sent_diff`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=238343961;

パッケージは作業量であり、送信されるのはタスクが実行された頻度です。 sent < packageの場合、ジョブはワーカーがそれを受け取るためにオープンです。 package_sent_diffは、クエリのパフォーマンスのために実行する必要がある量をインデックスするために使用されます。 performed_byは、ワーカーにジョブを割り当てるために使用されます。

MariaDBバージョン10.0.20、テーブルには200万行以上。

自動コミットはオンです

このキューに基づいていくつかのワーカーが実行されます。

単純にクエリの概要を説明するには:

ワーカーが更新を実行して、未使用の10個のジョブを要求します

UPDATE media 
SET performed_by = '71602155f18ac6001eb', 
  last_run = NOW() 
WHERE flag = 0 
  AND t_check < 75 
  AND sent < 20 
  AND last_run < NOW() 
  AND performed_by IS NULL 
  AND sent < 4377 
  AND package_sent_diff > 0 
LIMIT 10;

次に、ワーカーはテーブルからすべての情報を選択します

SELECT * FROM media WHERE performed_by = '71602155f18ac6001eb'

その後、ジョブを実行し、送信されたジョブの値を1つ増やし、後で実行するために行を解放します。

UPDATE media SET sent = sent + 1, performed_by = NULL WHERE mid = 238323961

この時点で、package_sent_diffキーでデッドロックが発生することがよくあります

簡略化されたコード:

$unique = uniqid();
$mysqli->query("UPDATE media SET performed_by = '".$unique."', last_run = NOW() WHERE flag = 0 AND t_check < 75 AND sent < 20 AND last_run < NOW() AND performed_by IS NULL AND sent < 4377 AND package_sent_diff > 0 LIMIT 10");
$query = $mysqli->query("SELECT * FROM media WHERE performed_by = '".$unique."'");

while($job = $query->fetc_assoc()) {
    if(doJob($job)) {
        $mysqli->query("UPDATE media SET sent = sent + 1, performed_by = NULL WHERE mid = ".$job['mid']);
    }
}

こちらがSHOW ENGINE INNODB STATUSの結果です

------------------------
LATEST DETECTED DEADLOCK
------------------------
2015-09-10 14:09:22 7f7f27fb6700
*** (1) TRANSACTION:
TRANSACTION 2544765863, ACTIVE 0 sec updating or deleting
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1184, 3 row lock(s), undo log entries 1
MySQL thread id 725683, OS thread handle 0x7f7f50fb6700, query id 989929718 localhost media_queue updating
UPDATE media SET sent = sent + 1, performed_by = NULL WHERE mid = 238323961
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 2565 page no 59448 n bits 768 index `package_sent_diff` of table `media_queue`.`media` trx table locks 1 total table locks 13  trx id 2544765863 lock_mode X locks gap before rec insert intention waiting lock hold time 0 wait time before grant 0 
*** (2) TRANSACTION:
TRANSACTION 2544765389, ACTIVE 1 sec fetching rows, thread declared inside InnoDB 2350
mysql tables in use 1, locked 1
3029 lock struct(s), heap size 357928, 25327 row lock(s)
MySQL thread id 725748, OS thread handle 0x7f7f27fb6700, query id 989929439 localhost media_queue updating
UPDATE media SET performed_by = '71602155f18ac6001eb', last_run = NOW() WHERE flag = 0 AND t_check < 75 AND sent < 20 AND last_run < NOW() AND performed_by IS NULL AND sent < 4377 AND package_sent_diff > 0 LIMIT 10
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 2565 page no 59448 n bits 768 index `package_sent_diff` of table `media_queue`.`media` trx table locks 1 total table locks 13  trx id 2544765389 lock_mode X lock hold time 0 wait time before grant 0 
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 2565 page no 55655 n bits 1192 index `package_sent_diff` of table `media_queue`.`media` trx table locks 1 total table locks 13  trx id 2544765389 lock_mode X waiting lock hold time 0 wait time before grant 0 
*** WE ROLL BACK TRANSACTION (1)

ジョブを要求するクエリとsent列を更新するクエリが互いに競合してデッドロックを引き起こしているようです。

より多くの労働者が実行しているほど、デッドロックが頻繁に発生します

私はよくアプリケーションを読んで、デッドロックを予期する必要があり、デッドロックを引き起こすクエリを単に再発行する必要がありますが、これでは結果が出ませんでした。

UPDATE media SET sent = sent + 1, performed_by = NULL WHERE mid = 238323961クエリを3回または20回繰り返したとしても、そのクエリで既にデッドロックが発生していると、常にデッドロックが発生します。

そのデッドロックを取り除くために、どうすれば変更できますか?クエリを高速に保つことが重要であり、あまり遅くすることなくそのテーブルで100以上のワーカーを実行できます。

//編集:

複合インデックスを追加した後

ALTER TABLE  `media_queue`.`media` ADD INDEX  `performed_by_package_sent_diff` (  `performed_by` ,  `package_sent_diff` ) COMMENT  '';

2番目のクエリを変更して

UPDATE media SET sent = sent + 3, performed_by = NULL WHERE mid = 243674295 AND performed_by = '92817855f2e21978b76'

デッドロックは10分の1ですが、それでもデッドロックは発生します。

これがSHOW ENGINE INNODB STATUSの出力です。

------------------------
LATEST DETECTED DEADLOCK
------------------------
2015-09-11 15:48:29 7f7f487ff700
*** (1) TRANSACTION:
TRANSACTION 2585082648, ACTIVE 0 sec updating or deleting
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1184, 3 row lock(s), undo log entries 1
MySQL thread id 748336, OS thread handle 0x7f7f255b7700, query id 1018467234 localhost media_queue updating
UPDATE media SET sent = sent + 1, performed_by = NULL WHERE mid = 244045072 AND performed_by = '11514555f2f24e094e7'
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 2587 page no 61522 n bits 784 index `performed_by_package_sent_diff` of table `media_queue`.`media` trx table locks 1 total table locks 41  trx id 2585082648 lock_mode X locks gap before rec insert intention waiting lock hold time 0 wait time before grant 0 
*** (2) TRANSACTION:
TRANSACTION 2585081924, ACTIVE 2 sec fetching rows, thread declared inside InnoDB 1852
mysql tables in use 1, locked 1
4559 lock struct(s), heap size 521768, 66353 row lock(s)
MySQL thread id 748490, OS thread handle 0x7f7f487ff700, query id 1018466800 localhost media_queue Searching rows for update
UPDATE media SET performed_by = '35359855f2f4a5a7eaf', last_run = NOW() WHERE flag = 0 AND t_check < 75 AND sent < 20 AND last_run < NOW() AND performed_by IS NULL AND sent < 4370 AND package_sent_diff > 0 LIMIT 10
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 2587 page no 61522 n bits 784 index `performed_by_package_sent_diff` of table `media_queue`.`media` trx table locks 1 total table locks 41  trx id 2585081924 lock_mode X lock hold time 1 wait time before grant 0 
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 2587 page no 73537 n bits 520 index `performed_by_package_sent_diff` of table `media_queue`.`media` trx table locks 1 total table locks 41  trx id 2585081924 lock_mode X waiting lock hold time 0 wait time before grant 0 
*** WE ROLL BACK TRANSACTION (1)

また、90%のケースでは、デッドロックが発生した後にクエリが繰り返された場合に、クエリを強制的に実行できます。

これは、最適化の範囲内ですか、それともデッドロックを完全に取り除くことができますか?

3
maddo7

ORDER BY midの追加mightデッドロックを防止します。

しかし、私の本当の答えは「それと生きる」ことです。つまり、デッドロックが発生したことを認識し、UPDATEを再実行します。あなたは少し遅くなりましたが、それ以外は害はありませんでした。

無関係:

KEY `sent` (`sent`),
KEY `sent_package` (`sent`,`package`),

それらの最初のものは冗長であり、削除できます。

1
Rick James

答えは、更新のwhere句に関連する主キーを読み取り、その主キーを使用してすべての行を個別に更新することを選択した場合に、更新を修正することでした。

私は約100 mariadbデータベース、それぞれ約32〜40 cpu、384gbramの環境で作業し、各ノードには6 TB〜33 TBのmariadbデータベースがあります。文字通り、ボリュームに応じて毎秒300,000〜500,000のトランザクションを実行します。

大量の環境で多くのサービスを再生できる唯一の方法は、すべての更新と削除を100%行うことです。主キーを使用する必要があります。行レベルのロックのみを取得する必要があります。これは、現在のmysqlデータベースで使用できる最も細かいロックセットです。

0
SAK