ロックについて調べてみた
共有ロックと排他ロックについて
DBにはデータの整合性を保つためにロックというものが存在します。
例えば衣服が売られているECサイトでTシャツの在庫が残り1着だったとします。そのTシャツをAさんとBさんが同時に買おうとした場合、1着ずつ同時に売れてしまい、1着しか在庫がないのにも関わらず2着売れてしまうというおかしなことになってしまいます。
このようなデータの矛盾を防ぎ整合性を保つために必要なのがロックになります。
DBのロックは共有ロックと排他ロックの大きく2種類あります。
この二つはロックのレベルがそれぞれ異なっています。
共有ロック:ロック対象に対して、参照以外のデータのアクセスをブロックする
排他ロック:ロック対象に全てのアクセスをブロックする
共有ロックは他のトランザクションからSELECTなどデータの取得はできますが、UPDATEやINSERTなどデータの書き込み処理は実行できません。
一方で排他ロックはSELECTを含め、全ての処理が実行できません。
なのでロックの強度レベルとしては
排他ロック>共有ロック
になります。
またロックには粒度も種類があります。
表ロック:表を対象にロックする
行ロック:行(レコード単位)か複数行にまたがってロックする
表ロックは大量のバッチ処理や厳密に整合性を保ちたい場合に使用します。
行ロックは通常のデータの更新で使用します。BETWEEN句など範囲でSELECTする場合は複数行にまたがってロックするギャップロックを実行します。
なぜ複数行に跨ってロックする必要があるのかは別の記事で触れたいと思います。
最初のECサイトの例でデータの矛盾を防ぐために以下のようなフローでロックをかけます。

ロックを使うことでデータの矛盾を防ぐことができました!
デッドロックについて
SQLを触ったことがある方ならデッドロックというワードを一度は目にしたことがあるかと思います。
デッドロックとは複数のトランザクションでロックが発生することにより待機状態が絡み合い、各トランザクションが身動きできなくなってしまう状態を指します。
実際にMySQLで銀行口座の処理を例にデッドロックを起こしてみます。
銀行口座テーブル(kouza)
| id | name | deposit_amount |
|---|---|---|
| 1 | A | 10000 |
| 2 | B | 30000 |
CREATE table kouza (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(20),
deposit_amount INT
);
INSERT into kouza (id,name,deposit_amount) VALUES
(1,'A',10000),
(2, 'B',30000);
コメントのSTEPで実行します。
-- Aの口座からBの口座へ振り込み
-- step1
BEGIN; -- トランザクション1開始
-- step2: id1のレコードをロック
SELECT * FROM kouza WHERE id = 1 FOR UPDATE;
-- step5 id2のレコードに対して更新(トランザクション2のロック解除待ち)
UPDATE kouza SET deposit_amount = deposit_amount + 5000 WHERE id = 2;
UPDATE kouza SET deposit_amount = deposit_amount - 5000 where id = 1;
-- Bの口座からAの口座に振り込み
-- step3 トランザクション2開始
BEGIN;
-- step4 id2のレコードをロック
SELECT * FROM test.kouza WHERE id = 2 FOR UPDATE;
-- step6 id1のレコードに対して更新処理(トランザクション1のロック解除待ち)。ここでデッドロックが起きる
UPDATE test.kouza SET deposit_amount = deposit_amount + 10000 WHERE id = 1;
UPDATE kouza SET deposit_amount = deposit_amount - 10000 where id = 2;
mysql> UPDATE test.kouza SET deposit_amount = deposit_amount + 10000 WHERE id = 1;
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
SELECT FOR UPDATEは行レベルでレコードを排他ロックします。
トランザクション1ではSELECT * FROM kouza WHERE id = 1 FOR UPDATE;でAの口座のレコードをロックします。
トランザクション2ではSELECT * FROM kouza WHERE id = 2 FOR UPDATE;でBの口座のレコードをロックします。
ここでidが1,2のレコードがそれぞれ排他ロックされた状態です。
そこでそれぞれのレコードを更新しようとすると、トランザクション1はトランザクション2のロック解除待ち、トランザクション2はトランザクション1のロック解除待ちになることでお互いがお互いのロック解除を待ってデッドロックが発生することになります。
こうなるとどうすることもできないので両方のトランザクションでROLLBACKする必要があります。
Discussion