Slide 1

Slide 1 text

MySQLのロックについて 〜基本編〜 2022.03.25

Slide 2

Slide 2 text

自己紹介 • 三谷 智史(@mita2) • 仕事その1 とある企業のMySQL チームの中間管理職 • 仕事その2 ANDPADのデータベース技術顧問 • 月に2日、お邪魔してます •   • http://mita2db.hateblo.jp/

Slide 3

Slide 3 text

本日のゴール ロック関連のエラーの解析ができるようになる

Slide 4

Slide 4 text

目次 1. ロックが必要な理由 2. ロックの範囲 • MySQLのレコードロック • 外部キーとロック 3. ロックに関するエラーと対処方法 • ロック待ちタイムアウト • デッドロック 4. 質疑応答 今日取り上げないこと ・DDLのロック、metadatalock ・ギャップロック/ネクストキーロック ・シャドーロック ・TX分離レベルがREPEATABLE-READ以外のロックの挙 動

Slide 5

Slide 5 text

目次 1. ロックが必要な理由 2. ロックの範囲 • MySQLのレコードロック • 外部キーとロック 3. ロックに関するエラーと対処方法 • ロック待ちタイムアウト • デッドロック 4. 質疑応答

Slide 6

Slide 6 text

なぜ、ロックが必要か? 複数のトランザクションを安全に並列実行するため

Slide 7

Slide 7 text

ロックのない世界 UPDATE zaiko_tbl SET 在庫 = 在庫-10 UPDATE zaiko_tbl SET 在庫 = 在庫-20 在庫を読み出す 在庫を読み出す 書き込むべき値を計算 (100 – 20) 値 (80) を書き込む 書き込むべき値を計算 (100 – 10) 値 (90) を書き込む 在庫 100 在庫 90 在庫 80 不整合!

Slide 8

Slide 8 text

ロックのある世界 在庫 100 UPDATE zaiko_tbl SET 在庫 = 在庫-10 UPDATE zaiko_tbl SET 在庫 = 在庫-20 在庫を読み出す 在庫を読み出す 書き込むべき値を計算 (90 – 20) 値 (70) を書き込む 書き込むべき値を計算 (100 – 10) 値 (90) を書き込む ロック解放待 ち 在庫 90 在庫 70 ロック 中 ロック 中

Slide 9

Slide 9 text

なぜ、ロックが必要か? • 高い性能(並列性) と安全性(データの整合性)の両立のため • 互いに影響しない処理は並行に実行 • 互いに影響しあう処理はロックによりシリアルに実行

Slide 10

Slide 10 text

目次 1. ロックが必要な理由 2. ロックの範囲 • MySQLのレコードロック • 外部キーとロック 3. ロックに関するエラーと対処方法 • ロック待ちタイムアウト • デッドロック 4. 質疑応答

Slide 11

Slide 11 text

レコードロック ■データ mysql> select * from lockt; +----+-------+----------+---------+ | pk | name | favorite | addr | +----+-------+----------+---------+ | 1 | Taro | Apple | Tokyo | | 2 | Jiro | Orange | Tokyo | | 3 | Alice | Orange | London | | 4 | John | Pine | NewYork | +----+-------+----------+---------+ 4 rows in set (0.00 sec) ■クエリー mysql> UPDATE lockt SET favorite = ‘Banana' WHERE favorite = 'Orange' AND addr = ‘Tokyo'; どの行がロックされ る? ■テーブル定義 CREATE TABLE `lockt` ( `pk` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `favorite` varchar(255) DEFAULT NULL, `addr` varchar(255) DEFAULT NULL, PRIMARY KEY (`pk`)) ENGINE=InnoDB;

Slide 12

Slide 12 text

レコードロック ■テーブル定義 CREATE TABLE `lockt` ( `pk` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `favorite` varchar(255) DEFAULT NULL, `addr` varchar(255) DEFAULT NULL, PRIMARY KEY (`pk`)) ENGINE=InnoDB; ■ロック mysql> UPDATE lockt SET favorite = ‘Banana' WHERE favorite = 'Orange' AND addr = ‘Tokyo'; +----+-------+----------+---------+ | pk | name | favorite | addr | +----+-------+----------+---------+ | 1 | Taro | Apple | Tokyo | | 2 | Jiro | Orange | Tokyo | | 3 | Alice | Orange | London | | 4 | John | Pine | NewYork | +----+-------+----------+---------+ 4 rows in set (0.00 sec)

Slide 13

Slide 13 text

レコードロック ■ロック mysql> UPDATE lockt SET favorite = ‘Banana' WHERE favorite = 'Orange' AND addr = ‘Tokyo'; +----+-------+----------+---------+ | pk | name | favorite | addr | +----+-------+----------+---------+ | 1 | Taro | Apple | Tokyo | | 2 | Jiro | Orange | Tokyo | | 3 | Alice | Orange | London | | 4 | John | Pine | NewYork | +----+-------+----------+---------+ 4 rows in set (0.00 sec) ■テーブル定義 CREATE TABLE `lockt` ( `pk` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `favorite` varchar(255) DEFAULT NULL, `addr` varchar(255) DEFAULT NULL, PRIMARY KEY (`pk`), KEY `idx_addr` (`addr`)) ENGINE=InnoDB;

Slide 14

Slide 14 text

レコードロック ■テーブル定義 CREATE TABLE `lockt` ( `pk` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `favorite` varchar(255) DEFAULT NULL, `addr` varchar(255) DEFAULT NULL, PRIMARY KEY (`pk`), KEY `idx_addr` (`addr`)) ENGINE=InnoDB; ■ロック mysql> UPDATE lockt SET favorite = ‘Banana' WHERE favorite = 'Orange' AND addr = ‘Tokyo'; +----+-------+----------+---------+ | pk | name | favorite | addr | +----+-------+----------+---------+ | 1 | Taro | Apple | Tokyo | | 2 | Jiro | Orange | Tokyo | | 3 | Alice | Orange | London | | 4 | John | Pine | NewYork | +----+-------+----------+---------+ 4 rows in set (0.00 sec) インデックス追加で ロックの粒度が変わる

Slide 15

Slide 15 text

レコードロック ■テーブル定義 CREATE TABLE `lockt` ( `pk` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `favorite` varchar(255) DEFAULT NULL, `addr` varchar(255) DEFAULT NULL, PRIMARY KEY (`pk`), KEY `idx_fav_addr` (`favorite`,`addr`) ) ENGINE=InnoDB; ■ロック mysql> UPDATE lockt SET favorite = ‘Banana' WHERE favorite = 'Orange' AND addr = ‘Tokyo'; +----+-------+----------+---------+ | pk | name | favorite | addr | +----+-------+----------+---------+ | 1 | Taro | Apple | Tokyo | | 2 | Jiro | Orange | Tokyo | | 3 | Alice | Orange | London | | 4 | John | Pine | NewYork | +----+-------+----------+---------+ 4 rows in set (0.00 sec)

Slide 16

Slide 16 text

レコードロック ■テーブル定義 CREATE TABLE `lockt` ( `pk` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `favorite` varchar(255) DEFAULT NULL, `addr` varchar(255) DEFAULT NULL, PRIMARY KEY (`pk`), KEY `idx_fav_addr` (`favorite`,`addr`) ) ENGINE=InnoDB; ■ロック mysql> UPDATE lockt SET favorite = ‘Banana' WHERE favorite = 'Orange' AND addr = ‘Tokyo'; +----+-------+----------+---------+ | pk | name | favorite | addr | +----+-------+----------+---------+ | 1 | Taro | Apple | Tokyo | | 2 | Jiro | Orange | Tokyo | | 3 | Alice | Orange | London | | 4 | John | Pine | NewYork | +----+-------+----------+---------+ 4 rows in set (0.00 sec)

Slide 17

Slide 17 text

実行計画 ■インデックスあり mysql> EXPLAIN UPDATE lockt SET favorite = 'Banana' WHERE favorite = 'Orange' AND addr = 'Tokyo'; +----+-------------+-------+-------+-------------------+-------------+------+ | id | select_type | table | type | key | ref | rows | +----+-------------+-------+-------+-------------------+-------------+------+ | 1 | SIMPLE | lockt | range | idx_favorite_addr | const,const | 1 | +----+-------------+-------+-------+-------------------+-------------+------+ 1 row in set (0.00 sec) ■インデックスなし mysql> EXPLAIN UPDATE lockt SET favorite = 'Banana' WHERE favorite = 'Orange' AND addr = 'Tokyo'; +----+-------------+-------+-------+---------+------+------+ | id | select_type | table | type | key | ref | rows | +----+-------------+-------+-------+---------+------+------+ | 1 | SIMPLE | lockt | index | PRIMARY | NULL | 4 | +----+-------------+-------+-------+---------+------+------+ 1 row in set (0.00 sec) フルスキャン

Slide 18

Slide 18 text

InnoDBのロック • 「条件に合致した行」に対するロックではなく、「スキャンしたインデック ス」に対するロック • ロック範囲は実行計画に依存する • 適切にチューニングされていないクエリは余計な行もロックする

Slide 19

Slide 19 text

TIPS:大量DELETE時のロック回避 • 普通にDELETE • 広範囲のレコードを長時間にわたってロック mysql> DELETE FROM table WHERE created_at < ‘2021-12-01’; +-------+-------+---------------+ | pk | name + created_at | +-------+-------+---------------+ | 1 | xxxx + 2021-10-03 | | 2 | xxxx + 2021-11-14 | : | 10000 | xxxx + 2021-11-30 | : | 20000 | xxxx + 2021-12-05 | +-------+-------+---------------+

Slide 20

Slide 20 text

TIPS:大量DELETE時のロック回避 • まず、対象をSELECT • 小分けにしてDELETE mysql> SELECT id FROM table WHERE created_at < ‘2021-12-01’; mysql> DELETE FROM table WHERE id IN (1,2,3,4….) +-------+-------+---------------+ | pk | name + created_at | +-------+-------+---------------+ | 1 | xxxx + 2021-10-03 | | 2 | xxxx + 2021-11-14 | : | 10000 | xxxx + 2021-11-30 | : | 20000 | xxxx + 2021-12-05 | +-------+-------+---------------+

Slide 21

Slide 21 text

目次 1. ロックが必要な理由 2. ロックの範囲 • MySQLのレコードロック • 外部キーとロック 3. ロックに関するエラーと対処方法 • ロック待ちタイムアウト • デッドロック 4. 質疑応答

Slide 22

Slide 22 text

外部キー • 子テーブルへINSERT、UPDATEした際、親テーブルの共有ロックを取る • 子テーブルのトランザクションが完了するまで、親テーブルの該当行が更新でき なくなる

Slide 23

Slide 23 text

外部キー ■userテーブル +----+-----------+ | pk | user_name | +----+-----------+ | 1 | user1 | | 2 | user2 | | 3 | user3 | +----+-----------+ ■user_itemテーブル +----+---------+-----------+ | pk | user_id | item_name | +----+---------+-----------+ | 1 | 1 | item1 | | 2 | 2 | item2 | +----+---------+-----------+ CREATE TABLE `user_item` ( `pk` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `user_id` bigint(20) unsigned NOT NULL, `item_name` varchar(255) NOT NULL, PRIMARY KEY (`pk`), UNIQUE KEY `pk` (`pk`), KEY `user_id` (`user_id`), CONSTRAINT `user_item_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`pk`) ) ENGINE=InnoDB

Slide 24

Slide 24 text

外部キー ■userテーブル +----+-----------+ | pk | user_name | +----+-----------+ | 1 | user1 | | 2 | user2 | | 3 | user3 | +----+-----------+ 3 rows in set (0.00 sec) mysql> BEGIN; mysql> INSERT INTO user_item     (pk, user_id, item_name) VALUES(3, 3, 'item3'); ロックされる

Slide 25

Slide 25 text

外部キー ■userテーブル +----+-----------+ | pk | user_name | +----+-----------+ | 1 | user1 | | 2 | user2 | | 3 | user3 | +----+-----------+ 3 rows in set (0.00 sec) mysql> BEGIN; mysql> UPDATE user_item SET user_id = 1 WHERE pk = 2; ロックされる

Slide 26

Slide 26 text

目次 1. ロックが必要な理由 2. ロックの範囲 • MySQLのレコードロック • 外部キーとロック 3. ロックに関するエラーと対処方法 • ロック待ちタイムアウト • デッドロック 4. 質疑応答

Slide 27

Slide 27 text

ロックに関するエラー • ERROR 1205 (HY000): Lock wait timeout exceeded • これはロック待ちでタイムアウト(デッドロックではない) • ERROR 1213 (40001): Deadlock found when trying to get lock • これはデッドロック

Slide 28

Slide 28 text

ロックに関するエラー • ERROR 1205 (HY000): Lock wait timeout exceeded • これはロック待ちでタイムアウト(デッドロックではない) • ERROR 1213 (40001): Deadlock found when trying to get lock • これはデッドロック

Slide 29

Slide 29 text

Lock wait timeout exceeded • 先行するトランザクションが保持しているロックの解放を待っている時間が innodb_lock_wait_timeout パラメータの値を超えるとタイムアウト • デフォルト値は innodb_lock_wait_timeout = 50 • デッドロックよりユーザ体験を損ねる • タイムアウトは50秒まってエラー • デッドロックは即時エラー

Slide 30

Slide 30 text

• 赤矢印部分の時間が innodb_lock_wait_timeout を超えるとT2はエラー Lock wait timeout exceeded TRANSACTION 1 TRANSACTION 2 BEGIN UPDATE 行A BEGIN UPDATE 行A 行A 時間 COMMIT ロック解放待 ち ロック保 持

Slide 31

Slide 31 text

Rails サンプルコード (1) def cleanup id = params[:id] histories = History.find([1, 10000000]) histories.destroy_all # すごい時間のかかるDELETE respond_to do |format| format.html { redirect_to ‘/tasks’, notice: ‘削除しました。’ } format.json { render :show, status: :created, location: task } end end • DELETE対象に対するロックで長時間待たされる

Slide 32

Slide 32 text

Rails サンプルコード (2) def update id = params[:id] task = Task.find(id) Task.transaction do # BEGIN task.save # UPDATE sleep 60 #時間のかかる(SQLではない)処理 end # COMMIT end • ロック保持期間 • 󰢃 クエリの実行中 • 󰢏 トランザクションの実行中

Slide 33

Slide 33 text

ロックタイムアウトの原因調査方法 • 基本的にはパフォーマンス問題 • innodb_lock_wait_timeout を超えるトランザクションが存在する

Slide 34

Slide 34 text

手がかりの見つけ方 1. Bugsnag でエラー発生時間と処理内容を確認する 2. Datadog APM でエラーを食らったSQLを確認する • 該当テーブル or 該当テーブルの子テーブル(FKありの場合)に対して、長時間の トランザクションが実行中であったはず 3. Cloud Watch Logs を利用して、スロークエリログから生SQLを入手する

Slide 35

Slide 35 text

Bugsnag • いつ発生したか?どんな処理・テーブルでエラーを食らったか? 社外非公開

Slide 36

Slide 36 text

Bugsnag • Bugsnag から Datadog の trace id を取る 社外非公開 社外非公開

Slide 37

Slide 37 text

Datadog • https://app.datadoghq.com/apm/trace/ • 実際のSQLが確認可能 • ここで確認できるのは「タイムアウトを食らったSQL」(被害者) • 原因は別 社外非公開

Slide 38

Slide 38 text

Datadog • APMで該当時間前後のアクセスログを Duration (実行時間) でソートする • エラー発生時刻以降に、50秒+αで正常終了している処理が原因の可能性が高い 原因っぽい 社外非公開

Slide 39

Slide 39 text

Cloud Watch Logs • 生ログが確認できる。顧客依存の問題の場合の手がかりに。 • スロークエリログにはエラーになったSQLも記録される • 手順 • 社内ドキュメントURL

Slide 40

Slide 40 text

ロックに関するエラー • ERROR 1205 (HY000): Lock wait timeout exceeded • これはロック待ちでタイムアウト(デッドロックではない) • ERROR 1213 (40001): Deadlock found when trying to get lock • これはデッドロック

Slide 41

Slide 41 text

デッドロックとは • 同じリソース(行)のロックを互いに取り合っている状態 • データベースの不具合ではない TRANSACTION 1 TRANSACTION 2 BEGIN UPDATE 行A UPDATE 行B BEGIN UPDATE 行B UPDATE 行A 行A 行B ロック獲得待ち ロック獲得待ち

Slide 42

Slide 42 text

デッドロックとは • 同じリソース(行)のロックを取り合っている状態 • データベースの不具合ではない TRANSACTION 1 TRANSACTION 2 BEGIN UPDATE 行A UPDATE 行B BEGIN UPDATE 行B UPDATE 行A 行A 行B デッドロック状態に陥った場合、 MySQLは片方のトランザクションを エラー(ロールバック)にし、もう片方を救う

Slide 43

Slide 43 text

デッドロックの解析方法

Slide 44

Slide 44 text

デッドロック解析手順 1. Bugsnag でデッドロックのログを確認する 2. 該当時間のDBのエラーログを Cloud Watch Logs から取得する 3. Bugsnag の情報とDBエラーログを突き合わせて、原因を特定する

Slide 45

Slide 45 text

Bugsnag • 「Deadlock found 〜」のログを検索する 社外非公開

Slide 46

Slide 46 text

Bugsnag • いつ発生したか?どんな処理・テーブルでエラーを食らったか? • ここでわかるのは 強制ロールバックされた側の情報。救われた側はエラーにはならない 社外非公開

Slide 47

Slide 47 text

Cloud Watch Logs • MySQL のエラーログを見る • Bugsnagでエラーが検知された時間帯にログが出ているはず ------------------------ LATEST DETECTED DEADLOCK ------------------------ 2020-06-18 15:32:07 7f76dd7c1700 *** (1) TRANSACTION: TRANSACTION 2464, ACTIVE 15 sec starting index read mysql tables in use 1, locked 1 LOCK WAIT 4 lock struct(s), heap size 1184, 6 row lock(s), undo log entries 1 MySQL thread id 6, OS thread handle 0x7f76dd77f700, query id 150 localhost root updating update lockt2 set name = 'hogehoge' where pk = 2

Slide 48

Slide 48 text

DBのエラーログの読み方(1) TRANSACTION 1 TRANSACTION 2 BEGIN クエリ? クエリ? 行? 行? クエリ? クエリ? • ログから競合したトランザクションの内容とロックの状況を特定する • 「強制ロールバックを食らった側」の処理は Bugsnag も手がかりになる ERR BEGIN COMMIT ? ? ? ? 社外非公開

Slide 49

Slide 49 text

DBのエラーログの読み方(2) ------------------------ LATEST DETECTED DEADLOCK ------------------------ 2020-06-18 15:32:07 7f76dd7c1700 *** (1) TRANSACTION: TRANSACTION 2464, ACTIVE 15 sec starting index read mysql tables in use 1, locked 1 LOCK WAIT 4 lock struct(s), heap size 1184, 6 row lock(s), undo log entries 1 MySQL thread id 6, OS thread handle 0x7f76dd77f700, query id 150 localhost root updating update lockt2 set name = 'hogehoge' where pk = 2 *** (2) TRANSACTION: TRANSACTION 2465, ACTIVE 5 sec starting index read mysql tables in use 1, locked 1 4 lock struct(s), heap size 1184, 6 row lock(s), undo log entries 1 MySQL thread id 4, OS thread handle 0x7f76dd7c1700, query id 151 localhost root updating update lockt1 set name = 'hogehoge' where pk = 2 *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 22 page no 3 n bits 72 index `GEN_CLUST_INDEX` of table `test`.`lockt2` trx id 2465 lock_mode X Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0 0: len 8; hex 73757072656d756d; asc supremum;; *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 23 page no 3 n bits 72 index `GEN_CLUST_INDEX` of table `test`.`lockt1` trx id 2465 lock_mode X waiting Record lock, heap no 2 PHYSICAL RECORD: n_fields 7; compact format; info bits 0 0: len 6; hex 000000000208; asc ;; 1: len 6; hex 00000000099b; asc ;; 2: len 7; hex ef000001c20110; asc ;; 3: len 8; hex 0000000000000001; asc ;; 4: len 4; hex 5461726f; asc Taro;; 5: len 5; hex 4170706c65; asc Apple;; 6: len 5; hex 546f6b796f; asc Tokyo;; *** WE ROLL BACK TRANSACTION (2) ・トランザクションの最後のクエリと対象テーブルを読みとく ・トランザクションの最後のクエリ以外は推測するしかない! TRANSACTION 1 TRANSACTION 2 BEGIN クエリ? UPDATE lockt2 lockt1 pk=2 lockt2 pk=2 クエリ? UPDATE lockt1 ERR BEGIN COMMIT ? ? ? ?

Slide 50

Slide 50 text

DBのエラーログの読み方(3) ------------------------ LATEST DETECTED DEADLOCK ------------------------ 2020-06-18 15:32:07 7f76dd7c1700 *** (1) TRANSACTION: TRANSACTION 2464, ACTIVE 15 sec starting index read mysql tables in use 1, locked 1 LOCK WAIT 4 lock struct(s), heap size 1184, 6 row lock(s), undo log entries 1 MySQL thread id 6, OS thread handle 0x7f76dd77f700, query id 150 localhost root updating update lockt2 set name = 'hogehoge' where pk = 2 *** (2) TRANSACTION: TRANSACTION 2465, ACTIVE 5 sec starting index read mysql tables in use 1, locked 1 4 lock struct(s), heap size 1184, 6 row lock(s), undo log entries 1 MySQL thread id 4, OS thread handle 0x7f76dd7c1700, query id 151 localhost root updating update lockt1 set name = 'hogehoge' where pk = 2 *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 22 page no 3 n bits 72 index `GEN_CLUST_INDEX` of table `test`.`lockt2` trx id 2465 lock_mode X Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0 0: len 8; hex 73757072656d756d; asc supremum;; *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 23 page no 3 n bits 72 index `GEN_CLUST_INDEX` of table `test`.`lockt1` trx id 2465 lock_mode X waiting Record lock, heap no 2 PHYSICAL RECORD: n_fields 7; compact format; info bits 0 0: len 6; hex 000000000208; asc ;; 1: len 6; hex 00000000099b; asc ;; 2: len 7; hex ef000001c20110; asc ;; 3: len 8; hex 0000000000000001; asc ;; 4: len 4; hex 5461726f; asc Taro;; 5: len 5; hex 4170706c65; asc Apple;; 6: len 5; hex 546f6b796f; asc Tokyo;; *** WE ROLL BACK TRANSACTION (2) ・どちらのトランザクションが強制ロールバックされたか TRANSACTION 1 TRANSACTION 2 BEGIN クエリ? UPDATE lockt2 lockt1 pk=2 lockt2 pk=2 クエリ? UPDATE lockt1 ERR BEGIN COMMIT ? ? ? ?

Slide 51

Slide 51 text

DBのエラーログの読み方(4) ------------------------ LATEST DETECTED DEADLOCK ------------------------ *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 22 page no 3 n bits 72 index `GEN_CLUST_INDEX` of table `test`.`lockt2` trx id 2464 lock_mode X waiting Record lock, heap no 2 PHYSICAL RECORD: n_fields 7; compact format; info bits 0 0: len 6; hex 000000000204; asc ;; 1: len 6; hex 000000000992; asc ;; 2: len 7; hex e8000001bd0110; asc ;; 3: len 8; hex 0000000000000001; asc ;; 4: len 4; hex 5461726f; asc Taro;; 5: len 5; hex 4170706c65; asc Apple;; 6: len 5; hex 546f6b796f; asc Tokyo;; *** (2) TRANSACTION: TRANSACTION 2465, ACTIVE 5 sec starting index read mysql tables in use 1, locked 1 4 lock struct(s), heap size 1184, 6 row lock(s), undo log entries 1 MySQL thread id 4, OS thread handle 0x7f76dd7c1700, query id 151 localhost root updating update lockt1 set name = 'hogehoge' where pk = 2 *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 22 page no 3 n bits 72 index `GEN_CLUST_INDEX` of table `test`.`lockt2` trx id 2465 lock_mode X Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0 0: len 8; hex 73757072656d756d; asc supremum;; *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 23 page no 3 n bits 72 index `GEN_CLUST_INDEX` of table `test`.`lockt1` trx id 2465 lock_mode X waiting Record lock, heap no 2 PHYSICAL RECORD: n_fields 7; compact format; info bits 0 0: len 6; hex 000000000208; asc ;; 1: len 6; hex 00000000099b; asc ;; 2: len 7; hex ef000001c20110; asc ;; 3: len 8; hex 0000000000000001; asc ;; 4: len 4; hex 5461726f; asc Taro;; 5: len 5; hex 4170706c65; asc Apple;; 6: len 5; hex 546f6b796f; asc Tokyo;; *** WE ROLL BACK TRANSACTION (2) ・どのテーブルのロックを待っているか ・複数のテーブルを扱っている場合や外部キーを利用している場合 にテーブルを絞り込むのに役立つ TRANSACTION 1 TRANSACTION 2 BEGIN クエリ? UPDATE lockt2 lockt1 pk=2 lockt2 pk=2 クエリ? UPDATE lockt1 ERR BEGIN COMMIT ? 待ち ? 待ち

Slide 52

Slide 52 text

DBのエラーログの読み方(5) ------------------------ LATEST DETECTED DEADLOCK ------------------------ 2020-06-18 15:32:07 7f76dd7c1700 *** (1) TRANSACTION: TRANSACTION 2464, ACTIVE 15 sec starting index read mysql tables in use 1, locked 1 LOCK WAIT 4 lock struct(s), heap size 1184, 6 row lock(s), undo log entries 1 MySQL thread id 6, OS thread handle 0x7f76dd77f700, query id 150 localhost root updating update lockt2 set name = 'hogehoge' where pk = 2 *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 22 page no 3 n bits 72 index `GEN_CLUST_INDEX` of table `test`.`lockt2` trx id 2464 lock_mode X waiting Record lock, heap no 2 PHYSICAL RECORD: n_fields 7; compact format; info bits 0 0: len 6; hex 000000000204; asc ;; 1: len 6; hex 000000000992; asc ;; 2: len 7; hex e8000001bd0110; asc ;; 3: len 8; hex 0000000000000001; asc ;; 4: len 4; hex 5461726f; asc Taro;; 5: len 5; hex 4170706c65; asc Apple;; 6: len 5; hex 546f6b796f; asc Tokyo;; *** (2) TRANSACTION: TRANSACTION 2465, ACTIVE 5 sec starting index read mysql tables in use 1, locked 1 4 lock struct(s), heap size 1184, 6 row lock(s), undo log entries 1 MySQL thread id 4, OS thread handle 0x7f76dd7c1700, query id 151 localhost root updating update lockt1 set name = 'hogehoge' where pk = 2 *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 22 page no 3 n bits 72 index `GEN_CLUST_INDEX` of table `test`.`lockt2` trx id 2465 lock_mode X Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0 0: len 8; hex 73757072656d756d; asc supremum;; *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 23 page no 3 n bits 72 index `GEN_CLUST_INDEX` of table `test`.`lockt1` trx id 2465 lock_mode X waiting Record lock, heap no 2 PHYSICAL RECORD: n_fields 7; compact format; info bits 0 0: len 6; hex 000000000208; asc ;; 1: len 6; hex 00000000099b; asc ;; 2: len 7; hex ef000001c20110; asc ;; 3: len 8; hex 0000000000000001; asc ;; 4: len 4; hex 5461726f; asc Taro;; 5: len 5; hex 4170706c65; asc Apple;; 6: len 5; hex 546f6b796f; asc Tokyo;; ・HOLDS THE LOCK(S) はTRANSACTION2側しか表示してくれ ない TRANSACTION 1 TRANSACTION 2 BEGIN クエリ? UPDATE lockt2 lockt1 pk=2 lockt2 pk=2 クエリ? UPDATE lockt1 ERR BEGIN COMMIT ? 待ち 待ち ロック

Slide 53

Slide 53 text

残りのクエリ推測するしかない • A) エラーになっているトランザク ション。アプリのログから辿れる。 テーブル lockt2 を含んでいるは ず(もしくは、lockt2 の子テーブ ル)。 • B) テーブル lockt1 を含んでいる はず(もしくは、lockt1 の子テー ブル)。 • C) T2が待っている lockt1 の ロックを保持 TRANSACTION 1 TRANSACTION 2 BEGIN クエリB UPDATE lockt2 lockt1 pk=2 lockt2 pk=2 クエリA UPDATE lockt1 ERR BEGIN COMMIT (C) 待ち 待ち ロック

Slide 54

Slide 54 text

答え TRANSACTION 1 TRANSACTION 2 BEGIN UPDATE lockt1 WHERE pk=2 UPDATE lockt2 WHERE pk=2 BEGIN UPDATE lockt2 WHERE pk=2 UPDATE lockt1 WHERE pk=2 lockt1 pk=2 lockt2 pk=2 ERR COMMIT

Slide 55

Slide 55 text

ANDPADでのdeadlock 例 • 1時間に1〜2件発生していた users.id = 320208 TRANSACTION (A) TRANSACTION (B) BEGIN UPDATE users BEGIN INSERT or UPDATE usersの子テーブル 外部キーによる 共有ロック UPDATE users 排他ロックが 取れない 排他ロックが 取れない

Slide 56

Slide 56 text

デッドロックの対処方法 1. トランザクションを再実行する(リトライ) • お手軽 • FE→API→DB なら、FEからAPIにリトライすると楽 2. ロック時間を短くする • SQLチューニング • BEGIN〜COMMIT の間で時間のかかる処理をしない • トランザクションを小さく(こまめにCOMMIT)する 3. ロックの取り合いを防ぐ • 更新する順序を揃える

Slide 57

Slide 57 text

まとめ

Slide 58

Slide 58 text

まとめ • MySQL (InnoDB) のロック範囲は実行計画に依存する • 外部キーを持つテーブルへの更新は親テーブルのロックを取る • ロックタイムアウトとデッドロックは別物 • デッドロックはエラーログから解析する • こまったら、Slack で 相談しよう! • #dev_dbパフォーマンスチューニング

Slide 59

Slide 59 text

参考資料 • MySQL SQLチューニング 勉強会 • 社内ドキュメント • MySQL ロック勉強会(DDL編) • https://tech.andpad.co.jp/entry/2020/08/18/132130 • Datadog APM を使ったパフォーマンス分析 勉強会 • https://tech.andpad.co.jp/entry/2021/02/10/170000 • データベース利用規約/SQLレビュー観点 • 社内ドキュメント

Slide 60

Slide 60 text

Thanks