Go to list of users who liked
Share on X(Twitter)
Share on Facebook
More than 1 year has passed since last update.
概要
RDB(リレーショナルデータベース)を運用していると、複数のトランザクションが同じデータに同時アクセスしようとする場合に「デッドロック」が発生することがあります。デッドロックとは、あるトランザクションが必要とするリソースが別のトランザクションによってロックされ、さらにそのトランザクションも他のリソースのロック解除を待っているため、互いに進行できなくなってしまう状態を指します。
デッドロックが発生すると、データベース全体のパフォーマンスに悪影響を及ぼし、最悪の場合は一部の操作が失敗する原因 になります。
文章だけではイメージしにくい場合もあるため、以下の図でデッドロックの概念を視覚的に説明します。
今回はそのデッドロックをわざと発生させて挙動を確認してみます。
1. 使用した環境情報
- MySQL: 8.4.3
- エンジン: InnoDB
- Dockerイメージ: mysql:8.4.3
1. MySQL準備
今回の環境はDokcerコンテナで準備します。
1.dokcer-compose.ymlの準備
version:'3.8'services:mysql:image:mysql:8.4.3environment:MYSQL_ROOT_PASSWORD:rootMYSQL_USER:userMYSQL_PASSWORD:passwordports:-"3306:3306"2. コンテナ起動
docker compose up-d3. プロセス確認及びコンテナに入る
docker psdocker exec -it [container ID] bash4. MySQLバージョンチェック
bash-5.1#mysql--versionmysqlVer8.4.3forLinuxonx86_64(MySQLCommunityServer-GPL)bash-5.1#2. データの準備
1. データベース作成
CREATEDATABASE`testdb`;USEtestdb;2. テーブル作成
-- テーブルA作成CREATETABLEtable_A(idINTAUTO_INCREMENTPRIMARYKEY,fieldDECIMAL(10,2)NOTNULL);-- テーブルB作成CREATETABLEtable_B(idINTAUTO_INCREMENTPRIMARYKEY,fieldDECIMAL(10,2)NOTNULL);3. データのインサート
INSERTINTOtable_A(field)VALUES(100.00),(200.00),(300.00);INSERTINTOtable_B(field)VALUES(100.00),(200.00),(300.00);4. データ確認
mysql>SELECT*FROMc;+----+--------+|id|field|+----+--------+|1|100.00||2|200.00||3|300.00|+----+--------+3rowsinset(0.00sec)mysql>SELECT*FROMtable_B;+----+--------+|id|field|+----+--------+|1|100.00||2|200.00||3|300.00|+----+--------+3rowsinset(0.01sec)3. デッドロックを発生させる
デッドロックは主に 排他ロック と 共有ロック の取り合いから発生します。以下の表は、それぞれのロックの互換性を簡単に示したものです。
| 共有ロック | 排他ロック | |
|---|---|---|
| 共有ロック | ◯ | ✗ |
| 排他ロック | ✗ | ✗ |
トランザクション1
最初に トランザクション1 の 処理A で table_A の id = 1 に排他ロックをかけてみます。
1. トランザクションの開始
STARTTRANSACTION;2. テーブルAのid=1に排他ロックをかけてみる
usetestdb;SELECT*FROMtable_AWHEREid=1FORUPDATE;3. ロックが取得できているかを確認
SHOWENGINEINNODBSTATUS;------------TRANSACTIONS------------Trxidcounter1855Purgedonefortrx's n:o < 1854 undo n:o < 0 state: running but idleHistory list length 0LIST OF TRANSACTIONS FOR EACH SESSION:---TRANSACTION 421755629867008, not started0 lock struct(s), heap size 1128, 0 row lock(s)---TRANSACTION 421755629865392, not started0 lock struct(s), heap size 1128, 0 row lock(s)---TRANSACTION 421755629864584, not started0 lock struct(s), heap size 1128, 0 row lock(s)---TRANSACTION 1854, ACTIVE 6 sec2 lock struct(s), heap size 1128, 1 row lock(s)MySQL thread id 13, OS thread handle 140280697189952, query id 108 localhost root User sleepSELECT *,SLEEP(3600) FROM table_A WHERE id = 1 FOR UPDATEトランザクション2
続いて、 トランザクション2 の 処理B で table_B の id = 1 に排他ロックをかけてみます。
1. トランザクションの開始
STARTTRANSACTION;2. テーブルBのid=1に排他ロックをかけてみる
usetestdb;SELECT*FROMtable_BWHEREid=1FORUPDATE;3. ロックが取得できているかを確認
SHOWENGINEINNODBSTATUS;先ほどの結果と比べてトランザクションが増えているのがわかると思います。
------------TRANSACTIONS------------Trxidcounter1856Purgedonefortrx's n:o < 1854 undo n:o < 0 state: running but idleHistory list length 0LIST OF TRANSACTIONS FOR EACH SESSION:---TRANSACTION 421755629867008, not started0 lock struct(s), heap size 1128, 0 row lock(s)---TRANSACTION 421755629865392, not started0 lock struct(s), heap size 1128, 0 row lock(s)---TRANSACTION 421755629864584, not started0 lock struct(s), heap size 1128, 0 row lock(s)---TRANSACTION 1855, ACTIVE 6 sec2 lock struct(s), heap size 1128, 1 row lock(s)MySQL thread id 16, OS thread handle 140280678315584, query id 121 localhost root User sleepSELECT *,SLEEP(3600) FROM table_B WHERE id = 1 FOR UPDATE---TRANSACTION 1854, ACTIVE 933 sec2 lock struct(s), heap size 1128, 1 row lock(s)MySQL thread id 13, OS thread handle 140280697189952, query id 108 localhost root User sleepSELECT *,SLEEP(3600) FROM table_A WHERE id = 1 FOR UPDATEデッドロック発生
次に、各トランザクションがさらに処理を進めようとするとデッドロックが発生します。
トランザクション1: 処理B で table_B にアクセスしようとするが、 トランザクション2 によってロックされています。
トランザクション2: 処理A で table_A にアクセスしようとするが、 トランザクション1 によってロックされています。
実際に処理してデッドロックを発生させてみます。
1. トランザクション1で処理Bを実行
SELECT*FROMtable_BWHEREid=1FORUPDATE;2. トランザクション2で処理Aを実行
SELECT*FROMtable_AWHEREid=1FORUPDATE;この操作により、デッドロックが発生し、トランザクション1の処理は以下のように正常に完了します。
mysql>SELECT*FROMtable_BWHEREid=1FORUPDATE;+----+--------+|id|field|+----+--------+|1|100.00|+----+--------+1rowinset(10.90sec)一方で、トランザクション2の処理はエラーとなります。
mysql>SELECT*FROMtable_AWHEREid=1FORUPDATE;ERROR1213(40001):Deadlockfoundwhentryingtogetlock;tryrestartingtransactionまとめ
デッドロックは、データベースを運用する上で避けられない問題の一つですが、今回のようにわざとデッドロックを発生させることで、実際の挙動やエラーメッセージを確認し、なぜ起こるのかが把握できただけでも怖いものではなくなると思います。
デッドロックを防ぐための一般的な対策として、以下のようなポイントがあります。
- トランザクションの順序を統一する
- トランザクションの粒度を小さくす
- 適切なロックレベルの選択
デッドロックが発生してもデータベースが自動的に検出し、該当するトランザクションを再実行することで影響を最小限に抑えられる場合もありますが、上記のように適切な設計を心がけることが重要です。
Register as a new user and use Qiita more conveniently
- You get articles that match your needs
- You can efficiently read back useful information
- You can use dark theme




