Movatterモバイル変換


[0]ホーム

URL:


LoginSignup
76

Go to list of users who liked

94

Share on X(Twitter)

Share on Facebook

Add to Hatena Bookmark

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

MySQLでわざとデッドロック発生させて挙動を確認してみた

Posted at

概要

RDB(リレーショナルデータベース)を運用していると、複数のトランザクションが同じデータに同時アクセスしようとする場合に「デッドロック」が発生することがあります。デッドロックとは、あるトランザクションが必要とするリソースが別のトランザクションによってロックされ、さらにそのトランザクションも他のリソースのロック解除を待っているため、互いに進行できなくなってしまう状態を指します。

デッドロックが発生すると、データベース全体のパフォーマンスに悪影響を及ぼし、最悪の場合は一部の操作が失敗する原因 になります。

文章だけではイメージしにくい場合もあるため、以下の図でデッドロックの概念を視覚的に説明します。

スクリーンショット 2024-11-04 0.33.44.png

今回はそのデッドロックをわざと発生させて挙動を確認してみます。

1. 使用した環境情報

  • MySQL: 8.4.3
  • エンジン: InnoDB
  • Dockerイメージ: mysql:8.4.3

1. MySQL準備

今回の環境はDokcerコンテナで準備します。

1.dokcer-compose.ymlの準備

docker-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-d

3. プロセス確認及びコンテナに入る

docker ps
docker exec -it [container ID] bash

4. 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 に排他ロックをかけてみます。

スクリーンショット 2024-11-04 12.31.19.png

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 に排他ロックをかけてみます。

スクリーンショット 2024-11-04 12.36.56.png

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 によってロックされています。

スクリーンショット 2024-11-04 0.33.44.png

実際に処理してデッドロックを発生させてみます。

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

まとめ

デッドロックは、データベースを運用する上で避けられない問題の一つですが、今回のようにわざとデッドロックを発生させることで、実際の挙動やエラーメッセージを確認し、なぜ起こるのかが把握できただけでも怖いものではなくなると思います。

デッドロックを防ぐための一般的な対策として、以下のようなポイントがあります。

  • トランザクションの順序を統一する
  • トランザクションの粒度を小さくす
  • 適切なロックレベルの選択

デッドロックが発生してもデータベースが自動的に検出し、該当するトランザクションを再実行することで影響を最小限に抑えられる場合もありますが、上記のように適切な設計を心がけることが重要です。

76

Go to list of users who liked

94
0

Go to list of comments

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
76

Go to list of users who liked

94

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?


[8]ページ先頭

©2009-2025 Movatter.jp