排他制御とは
データベースを扱う際、避けて通れないのが 排他制御 です。 例えば、残り1点の商品があるECサイトで、ユーザーAとユーザーBがほぼ同時に注文ボタンを押したとします。
もし排他制御がなければ、システムは両方の注文を成功とみなしてしまい、存在しない2点目の商品が売れてしまうことになります。 これを防ぐためには、ユーザーAが処理している間はユーザーBの操作を制限する必要があります。この仕組みを 排他制御 と言います。
この排他制御を実現するための具体的な手段が ロック です。
ロックはデータに鍵をかけるようなもので、あるトランザクションがデータを操作している間、他のトランザクションがそのデータにアクセスできないように制限します。
ロックの考え方と種類
前提として、データベースのロックには以下2種類があります。
- 楽観的・悲観的ロック
- 共有・排他ロック
同じ「ロック」という言葉を使っているため紛らわしいですが、両者は別軸の概念です。
前者の楽観的・悲観的ロックはロックを制御する「手法」を指し、後者の共有・排他ロックはロックの「種類」を指しております。
例えば「悲観的ロックを実現するために、排他ロックをかける」といった形で組み合わせて使われます。
共有ロック・排他ロック
共有ロック
データの読み込みは複数同時に実行できますが、データの書き込みはロックします。(別名READロック)
排他ロック
データの読み込みも、書き込みもロックします。(別名WRITEロック)
両者の比較
両者の違いはロックがかかっている間、他の処理に何を許可するかにあります。
詳しくは以下表の通りです。
| 種類 | 用途 | 読み取り | 書き込み |
|---|---|---|---|
| 共有ロック | データの読み取り | ○(可能) | ×(待機) |
| 排他ロック | データの書き込み | ×(待機) | ×(待機) |
楽観的・悲観的ロック
楽観的ロック
「データの競合は滅多に起こらない」という楽観的な前提に基づき、更新する直前までロックをかけない手法です。
実際には、データのバージョン番号などが読み取り時と変わっていないか、更新時にチェックします。
処理待ちは起こりづらいですが、先に他者が更新していた場合はエラーとなり、処理は最初からやり直しとなります。
悲観的ロック
「データは競合するもの」という悲観的な前提に基づき、データを読み取る時点でロックする手法です。
最初にロックして他者の割り込みを防ぐため、確実に処理を完了できます。
一方で処理中は他者を待たせることになるため、楽観的ロックと比べるとパフォーマンスが低下する可能性があります。
両者の違い
両者の違いは、以下トランザクションの基本的な流れ(BEGIN → 処理 → COMMIT)に当てはめた時、どの段階でチェック(またはロック)するかで区別できます。
①トランザクション開始(BEGIN)
②データ読み込み(SELECT)
③データ書き込み(UPDATE, DELETE等)
④トランザクション確定(COMMIT)
楽観的ロックは、「④トランザクション確定(COMMIT)」の時にチェックします。
悲観的ロックは、「②データの読み取り(SELECT)」の時にロックをかけます。
※BEGIN,COMMIT について詳しく知りたい方はトランザクションをちゃんと理解する【基礎編】 もご一読いただけますと幸いです。
デッドロック
トランザクションを扱う際は、 デッドロック に注意する必要があります。
デッドロックは「お互いの処理を待ち合い、処理が進まなくなること」を指しています。
例えば、銀行口座とポイントの2つのDBを更新する処理を考えます。
以下のように、トランザクションA,Bが異なる順番でDBにアクセスすると、デッドロックが発生します。
| 順序 | トランザクションA | トランザクションB |
|---|---|---|
| 1 | 処理開始 | 処理開始 |
| 2 | 銀行口座をロック | ポイント更新をロック |
| 3 | Bの処理待ちで、ポイント更新不可 | Aの処理待ちで、銀行口座更新不可 |
AはBによるポイントのロック解除、BはAによる銀行口座のロック解除を互いに待ち合う状態になると、DB側が検知して片方の処理を強制終了(ロールバック)させない限り、処理は永久に止まってしまいます。


