web-dev-qa-db-ja.com

このクエリでデッドロックが発生するのはなぜですか?

以下に、生のMySQLクエリと、それをプログラムで実行するコードを示します。 2つの要求が同時に行われている場合、次のエラーパターンが発生します。

SQLSTATE [40001]:シリアル化エラー:1213ロックを取得しようとしたときにデッドロックが見つかりました。トランザクションを再起動してみてください(SQL:update user_chats set updated_at = 2018-06-29 10:07:13 where id = 1

同じクエリを実行しますが、トランザクションブロックがない場合は、多くの同時呼び出しでエラーなしに機能します。どうして ? (トランザクションはロックを取得しますよね?)

テーブル全体をロックせずにこれを解決する方法はありますか? (テーブルレベルのロックを回避したい)

InnoDBを使用してMySqlでテーブルを挿入/更新/削除するためにロックが取得されることはわかっていますが、デッドロックがここで発生する理由と、最も効率的な方法でそれを解決する方法がわかりません。

    START TRANSACTION;

    insert into `user_chat_messages` (`user_chat_id`, `from_user_id`, `content`)
        values (1, 2, 'dfasfdfk);
    update `user_chats`
        set `updated_at` = '2018-06-28 08:33:14' where `id` = 1;

    COMMIT;

上記は生のクエリですが、PHP Laravel Query Builderで次のようにしています。

    /**
     * @param UserChatMessageEntity $message
     * @return int
     * @throws \Exception
     */
    public function insertChatMessage(UserChatMessageEntity $message) : int
    {
        $this->db->beginTransaction();
        try
        {
            $id = $this->db->table('user_chat_messages')->insertGetId([
                    'user_chat_id' => $message->getUserChatId(),
                    'from_user_id' => $message->getFromUserId(),
                    'content' => $message->getContent()
                ]
            );

            //TODO results in lock error if many messages are sent same time
            $this->db->table('user_chats')
                ->where('id', $message->getUserChatId())
                ->update(['updated_at' => date('Y-m-d H:i:s')]);

            $this->db->commit();
            return $id;
        }
        catch (\Exception $e)
        {
            $this->db->rollBack();
            throw  $e;
        }
    }

テーブルのDDL:

CREATE TABLE user_chat_messages
(
    id INT(10) unsigned PRIMARY KEY NOT NULL AUTO_INCREMENT,
    user_chat_id INT(10) unsigned NOT NULL,
    from_user_id INT(10) unsigned NOT NULL,
    content VARCHAR(500) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
    CONSTRAINT user_chat_messages_user_chat_id_foreign FOREIGN KEY (user_chat_id) REFERENCES user_chats (id),
    CONSTRAINT user_chat_messages_from_user_id_foreign FOREIGN KEY (from_user_id) REFERENCES users (id)
);
CREATE INDEX user_chat_messages_from_user_id_index ON user_chat_messages (from_user_id);
CREATE INDEX user_chat_messages_user_chat_id_index ON user_chat_messages (user_chat_id);


CREATE TABLE user_chats
(
    id INT(10) unsigned PRIMARY KEY NOT NULL AUTO_INCREMENT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);
7
Kristi Jorgji

この状況では、FOREIGN KEY _user_chat_messages_user_chat_id_foreign_がデッドロックの原因です。

幸い、あなたが提供した情報があれば、これは簡単に再現できます。

セットアップ

_CREATE DATABASE dba210949;
USE dba210949;

CREATE TABLE user_chats
(
    id INT(10) unsigned PRIMARY KEY NOT NULL AUTO_INCREMENT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);

CREATE TABLE user_chat_messages
(
    id INT(10) unsigned PRIMARY KEY NOT NULL AUTO_INCREMENT,
    user_chat_id INT(10) unsigned NOT NULL,
    from_user_id INT(10) unsigned NOT NULL,
    content VARCHAR(500) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
    CONSTRAINT user_chat_messages_user_chat_id_foreign FOREIGN KEY (user_chat_id) REFERENCES user_chats (id)
);

insert into user_chats (id,updated_at) values (1,NOW());
_

usersテーブルを参照するため、_user_chat_messages_from_user_id_foreign_外部キーを削除したことに注意してください。これは、この例ではありません。問題を再現することは重要ではありません。

デッドロックを再現する

接続1

_USE dba210949;
START TRANSACTION;
insert into `user_chat_messages` (`user_chat_id`, `from_user_id`, `content`) values (1, 2, 'dfasfdfk');
_

接続2

_USE dba210949;
START TRANSACTION;
insert into `user_chat_messages` (`user_chat_id`, `from_user_id`, `content`) values (1, 2, 'dfasfdfk');
_

接続1

_update `user_chats` set `updated_at` = '2018-06-28 08:33:14' where `id` = 1;
_

この時点で、接続1は待機しています。

接続2

_update `user_chats` set `updated_at` = '2018-06-28 08:33:14' where `id` = 1;
_

ここで、接続2はデッドロックをスローします

エラー1213(40001):ロックを取得しようとしたときにデッドロックが見つかりました。トランザクションを再開してみてください

外部キーなしで再試行する

同じ手順を繰り返しますが、次のテーブル構造を使用します。今回の唯一の違いは、_user_chat_messages_user_chat_id_foreign_外部キーの削除です。

_CREATE DATABASE dba210949;
USE dba210949;

CREATE TABLE user_chats
(
    id INT(10) unsigned PRIMARY KEY NOT NULL AUTO_INCREMENT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);

CREATE TABLE user_chat_messages
(
    id INT(10) unsigned PRIMARY KEY NOT NULL AUTO_INCREMENT,
    user_chat_id INT(10) unsigned NOT NULL,
    from_user_id INT(10) unsigned NOT NULL,
    content VARCHAR(500) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);

insert into user_chats (id,updated_at) values (1,NOW());
_

以前と同じ手順を再現

接続1

_USE dba210949;
START TRANSACTION;
insert into `user_chat_messages` (`user_chat_id`, `from_user_id`, `content`) values (1, 2, 'dfasfdfk');
_

接続2

_USE dba210949;
START TRANSACTION;
insert into `user_chat_messages` (`user_chat_id`, `from_user_id`, `content`) values (1, 2, 'dfasfdfk');
_

接続1

_update `user_chats` set `updated_at` = '2018-06-28 08:33:14' where `id` = 1;
_

この時点で、以前のように待機する代わりに、接続1が実行されます。

接続2

_update `user_chats` set `updated_at` = '2018-06-28 08:33:14' where `id` = 1;
_

接続2は現在待機している接続ですが、デッドロックは発生していません。

接続1

_commit;
_

接続2は待機を停止し、コマンドを実行します。

接続2

_commit;
_

デッドロックなしで完了。

どうして?

_SHOW ENGINE INNODB STATUS_の出力を見てみましょう

_------------------------
LATEST DETECTED DEADLOCK
------------------------
2018-07-04 10:38:31 0x7fad84161700
*** (1) TRANSACTION:
TRANSACTION 42061, ACTIVE 55 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 5 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 2, OS thread handle 140383222380288, query id 81 localhost root updating
update `user_chats` set `updated_at` = '2018-06-28 08:33:14' where `id` = 1
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 3711 page no 3 n bits 72 index PRIMARY of table `dba210949`.`user_chats` trx id 42061 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 4; hex 00000001; asc     ;;
 1: len 6; hex 00000000a44b; asc      K;;
 2: len 7; hex b90000012d0110; asc     -  ;;
 3: len 4; hex 5b3ca335; asc [< 5;;
 4: len 4; hex 5b3ca335; asc [< 5;;

*** (2) TRANSACTION:
TRANSACTION 42062, ACTIVE 46 sec starting index read
mysql tables in use 1, locked 1
5 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 3, OS thread handle 140383222109952, query id 82 localhost root updating
update `user_chats` set `updated_at` = '2018-06-28 08:33:14' where `id` = 1
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 3711 page no 3 n bits 72 index PRIMARY of table `dba210949`.`user_chats` trx id 42062 lock mode S locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 4; hex 00000001; asc     ;;
 1: len 6; hex 00000000a44b; asc      K;;
 2: len 7; hex b90000012d0110; asc     -  ;;
 3: len 4; hex 5b3ca335; asc [< 5;;
 4: len 4; hex 5b3ca335; asc [< 5;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 3711 page no 3 n bits 72 index PRIMARY of table `dba210949`.`user_chats` trx id 42062 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 4; hex 00000001; asc     ;;
 1: len 6; hex 00000000a44b; asc      K;;
 2: len 7; hex b90000012d0110; asc     -  ;;
 3: len 4; hex 5b3ca335; asc [< 5;;
 4: len 4; hex 5b3ca335; asc [< 5;;

*** WE ROLL BACK TRANSACTION (2)
_

トランザクション1には_user_chats_のPRIMARYキーにlock_mode Xがあり、トランザクション2にはlock_mode Sがあり、lock_mode X。これは、最初に(INSERTステートメントから)共有ロックを取得し、次に(UPDATEから)排他ロックを取得した結果です。

したがって、何が起こっているかというと、接続1が最初に共有ロックを取得し、次に接続2が同じレコードの共有ロックを取得しています。どちらも共有ロックなので、現時点では問題ありません。

次に、接続1は、UPDATEを実行するために排他ロックにアップグレードしようとしますが、接続2にはすでにロックが設定されていることを確認します。共有ロックと排他ロックは、名前で推測できるため、うまく混合できません。そのため、接続1でUPDATEコマンドの後に待機します。

次に、接続2はUPDATEを試行しますが、これには排他ロックが必要であり、InnoDBは「自分でこの状況を修正することはできません」とデッドロックを宣言します。接続2を強制終了し、接続2が保持していた共有ロックを解放し、接続1を正常に完了させます。

ソリューション

この時点で、あなたはおそらく、うんうんうんうんと立ち止まる準備ができており、解決策が必要です。ここに私の個人的な好みの順に、私の提案があります。

1.更新を完全に回避する

_updated_at_テーブルの_user_chats_列を気にしないでください。代わりに、列の複合インデックスを_user_chat_messages_に追加します(_user_chat_id_、_created_at_)。

_ALTER TABLE user_chat_messages
ADD INDEX `latest_message_for_user_chat` (`user_chat_id`,`created_at`)
_

その後、次のクエリで最新の更新時刻を取得できます。

_SELECT MAX(created_at) AS created_at FROM user_chat_messages WHERE user_chat_id = 1
_

このクエリは、インデックスが原因で非常に高速に実行され、最新の_updated_at_時間を_user_chats_テーブルに格納する必要もありません。これはデータの重複を回避するのに役立ちます。そのため、これが私の推奨ソリューションです。

私の例のように、id$message->getUserChatId()値に動的に設定し、_1_にハードコードしないでください。

これは本質的にリック・ジェームズが示唆していることです。

2.リクエストをシリアル化するためにテーブルをロックします

_SELECT id FROM user_chats WHERE id=1 FOR UPDATE
_

この_SELECT ... FOR UPDATE_をトランザクションの先頭に追加すると、リクエストがシリアル化されます。前と同じように、動的にid$message->getUserChatId()の値に動的に設定し、私の例のように_1_にハードコードしないでください。

これがジェラール・H・ピルの提案です。

3.外部キーを削除する

デッドロックの原因を取り除く方が簡単な場合もあります。 _user_chat_messages_user_chat_id_foreign_外部キーをドロップするだけで、問題は解決します。

データの整合性(外部キーが提供する)が好きなので、このソリューションは一般的にあまり好きではありませんが、トレードオフを行う必要がある場合があります。

4.デッドロック後にコマンドを再試行します

これは、一般的なデッドロックの推奨ソリューションです。エラーをキャッチして、リクエスト全体を再試行してください。ただし、最初から準備しておけば実装が最も簡単であり、レガシーコードの更新は難しい場合があります。より簡単な解決策(上記の1と2のような)があるという事実を考えると、これがあなたの状況に対して私の最も推奨されない解決策である理由です。

14
Willem Renzema

トランザクションの最初のステップとして、$ this-> db-> table( 'user_chats')-> where( 'id'、$ message-> getUserChatId())をロックします。これにより、デッドロックが回避されます。

0
Gerard H. Pille

user_chatsに1行しかない場合そうでない場合、idのセマンティクスは何ですか? 「ユーザー」ですか?または「チャット番号」?または、他の何か?

すべての接続が最後のチャット(id = 1)のIDをバンプしようとしているようです。それが必要な場合は、UPDATEを投げて、最新の日付が必要なときに代わりにこれを行うことを検討してください。

SELECT MAX(created_at) FROM user_chat_messages.
0
Rick James