MySQLのトランザクションとロック機構:在庫管理から学ぶデータ整合性
データベースを用いたシステム開発において、複数ユーザーからの同時アクセスを正しく処理することは非常に重要です。本記事では、MySQLにおけるデータの可視性、分離レベル、ロックの仕組み、そしてECサイトの決済処理における実践的な在庫管理の設計について解説します。
1. トランザクションとデータの可視性
データベースにデータを追加・更新する際、処理が完了するまでは他のユーザーからその変更を見えなくする必要があります。
-
未確定のデータは他者から見えない: トランザクションを開始してデータ操作を行った場合、
COMMIT(確定)を行うまでは他のセッションからは元のデータのまま見えます。 - MVCC(多版型同時実行制御): MySQLのInnoDBエンジンは、変更前のデータ(スナップショット)を保持しています。これにより、誰かがデータを更新している最中でも、他のユーザーは待たされることなく古いデータを読み取ることができます。
2. MySQLの分離レベル(Isolation Level)
分離レベルは、トランザクション同士が互いにどの程度影響を与えるかを定義するものです。
- REPEATABLE READ(MySQLのデフォルト): トランザクション内で最初にデータを読み取った時点の状態が最後まで維持されます。他者がデータを更新してコミットしても、自分の処理中はその影響を受けません。
- READ COMMITTED(多くの他DBのデフォルト): 他者がコミットした最新のデータを常に読み取ります。同一トランザクション内でも、読み取るタイミングによってデータが変わる可能性があります。
3. 悲観的ロックと楽観的ロック
データの不整合(例:在庫数の消失更新)を防ぐためのアプローチは2つあります。
悲観的ロック(物理的なロック)
「他者からも同時に更新される」という前提に立ち、データベースの機能でロックをかけます。
- 排他ロック(FOR UPDATE): 自分だけが占有するロックです。取得中は、他者はロックを伴う参照や更新ができず待機状態になります。在庫を減らす際など、確実に最新データを元に計算を行う場合に必須です。
- 共有ロック(FOR SHARE): 読み取り専用の相乗りロックです。同時に複数人が取得できますが、互いに更新に進もうとするとデッドロックを引き起こすため、在庫更新のような処理には不向きです。
楽観的ロック(論理的なロック)
「同時更新は滅多に起こらない」という前提に立ち、物理的なロックは使いません。
-
仕組み: テーブルに
versionカラムを持たせ、更新時にバージョンが一致するかを確認します。 - 利点と欠点: ロック待ちが発生しないためパフォーマンスに優れますが、競合が発生した場合(0件更新になった場合)は、アプリケーション側で再試行やエラーハンドリングを行う必要があります。
4. 普通のSELECTとロック付きSELECTの違い
REPEATABLE READ において、データの読み取り方によって挙動が変わります。
- **通常の
SELECT**: ロックを無視し、誰かが更新中であっても「更新前の確定データ」を即座に読み取ります(Consistent Read)。 -
SELECT ... FOR UPDATE: 後で更新するために最新状態を確保したい場合に使用します。誰かが更新中であれば、その処理が終わるまで待機します(Locking Read)。
5. 実践:決済と在庫更新の正しい設計
ECサイト等で決済API(クレジットカード引き落とし等)と在庫更新を組み合わせる場合、処理順序を誤ると「決済は成功したが在庫がなく、DBも更新できない」という致命的な不整合が発生します。
また、外部APIとの通信中に排他ロックを保持し続けると、システム全体の停止を招くため避けるべきです。
ベストプラクティス:在庫の仮押さえ(Reservation)
- 仮押さえ: 排他ロックを用いて在庫を減算し、注文ステータスを「決済待ち(Pending)」として一度コミット(ロック解除)します。
- 決済処理: 外部APIを呼び出し、引き落としを実行します。
- 結果反映:
- 成功時:注文ステータスを「完了」にする。
- 失敗時:在庫を元に戻し、注文ステータスを「失敗」にする。
この設計により、ロックの占有時間を最小限に抑えつつ、決済と在庫の確実な連動を実現できます。