データベースを複数のユーザーが同時に使う環境では、データの整合性を保つための仕組みが欠かせません。
その中心的な役割を担うのが「ロック」です。
「デッドロックって何が起きているの?」「共有ロックと排他ロックはどう違うの?」「分離レベルとロックはどう関係するの?」といった疑問をお持ちの方に向けて、本記事では丁寧に解説していきます。
トランザクションのロックとは?仕組みと基本概念
それではまず、データベースにおけるロックの基本的な仕組みと役割について解説していきます。
ロックとは、複数のトランザクションが同じデータに同時アクセスする際に、データの整合性を保つために「アクセスを制限・管理する仕組み」のことです。
例えば、AさんとBさんが同じ商品の在庫数を同時に更新しようとした場合、ロックがなければ両者の更新が干渉し合って正しい在庫数が保てなくなります。
ロックはこの問題を防ぐために、「今このデータを使っています、他の人は待ってください」という排他制御(Exclusive Control)を実現します。
ロックの主な種類と特徴
共有ロック(Shared Lock / S Lock):読み取り操作に使われます。複数のトランザクションが同時に共有ロックを取得できますが、排他ロックとは共存できません。
排他ロック(Exclusive Lock / X Lock):書き込み操作に使われます。1つのトランザクションのみ取得でき、他のいかなるロックとも共存できません。
行ロック(Row Lock):テーブルの特定の行だけをロックします。並行処理性能が高い細粒度のロックです。
テーブルロック(Table Lock):テーブル全体をロックします。粗粒度のロックで、大量データの一括処理に使われます。
ロックの粒度(どの単位でロックをかけるか)は、並行処理性能とデータ保護のバランスに大きく影響します。
細粒度(行ロック)ほど同時実行性が高まりますが、管理コストが増え、粗粒度(テーブルロック)ほど管理は簡単ですが他のトランザクションの待機時間が長くなります。
共有ロックと排他ロックの関係
共有ロックと排他ロックの互換性を整理すると、以下のようになります。
| ロックの種類 | 共有ロック(読み取り) | 排他ロック(書き込み) |
|---|---|---|
| 共有ロックと同時取得 | 可能 | 不可 |
| 排他ロックと同時取得 | 不可 | 不可 |
つまり、読み取り同士は干渉しませんが、書き込みが絡むと必ずロックの競合が発生します。
この特性を理解しておくことが、ロックの動作を正しく把握するための基礎です。
行ロックとテーブルロック
MySQLのInnoDBエンジンでは、デフォルトで行ロック(Row-level Lock)が使われます。
行ロックは特定の行だけをロックするため、異なる行に対するトランザクションが並行して実行でき、高い並行処理性能を実現します。
一方、MyISAMエンジンではテーブルロックが使われるため、1件の更新でもテーブル全体がロックされてしまい、並行処理性能が大きく劣ります。
大量アクセスが見込まれるWebサービスのデータベースでは、行ロックをサポートするInnoDBの選択が事実上の標準となっています。
デッドロックとは?発生原因と対処法
続いては、ロックに関連する最も重要な問題のひとつ、「デッドロック」について確認していきます。
デッドロックの仕組みと具体例
デッドロック(Deadlock)とは、2つ以上のトランザクションがそれぞれ相手が持つロックを待ち続け、永遠に処理が進まなくなる状態のことです。
【デッドロック発生の具体例】
トランザクションA:テーブル1の行αをロック → テーブル2の行βを取得しようとする
トランザクションB:テーブル2の行βをロック → テーブル1の行αを取得しようとする
→ AはBが持つロックを待ち、BはAが持つロックを待つ → 永遠に進まない(デッドロック)
デッドロックは人間の交渉の「膠着状態」と似ており、どちらも相手が先に譲ることを待っているために前に進めない状況です。
データベースは定期的にデッドロックを検出し、デッドロックに関わるトランザクションの一方を強制的にロールバックすることで解消します。
ロールバックされた側のアプリケーションはエラーを受け取り、処理を再試行する実装が必要になります。
デッドロックを防ぐための設計指針
デッドロックは完全に防ぐことは難しいですが、発生確率を大幅に下げる設計指針があります。
| 対策 | 説明 |
|---|---|
| ロック順序の統一 | 複数のリソースをロックする順序を常に同じにする(例:必ずテーブル1→2の順) |
| トランザクションの短縮 | ロック保持時間を最小にする(必要なデータだけをロック) |
| ロック粒度の適正化 | 必要最小限の粒度(行ロック)でロックする |
| タイムアウトの設定 | 一定時間待ってもロックが取得できない場合は自動的にロールバックする |
| インデックスの最適化 | インデックス不足でテーブルスキャンが走ると範囲ロックが広がりデッドロックリスクが増える |
最も基本的かつ効果的な対策は「複数のリソースへのロック順序をすべてのトランザクションで統一すること」です。
順序が統一されていれば、循環待ちが発生しなくなるためデッドロックを防げます。
デッドロック検出とロックタイムアウト
MySQLのInnoDBはデッドロック検出機能を持っており、循環待ちを検出すると一方のトランザクションを自動的にロールバックします。
innodb_lock_wait_timeoutパラメーターを設定することで、ロック待ちのタイムアウト時間を制御することも可能です。
デフォルトは50秒に設定されていますが、業務要件に応じて短縮することで、デッドロック解消を迅速化できます。
ロックと分離レベルの関係
続いては、ロックとトランザクションの分離レベルの関係について確認していきます。
分離レベルはACID特性の「I(独立性)」を制御する設定で、ロックの挙動に直接影響します。
分離レベルとロックの強度の関係
分離レベルが高くなるほど、より厳格なロック管理が行われ、データの正確性は向上しますが、並行処理性能は低下します。
| 分離レベル | ロックの特徴 | 防げる問題 |
|---|---|---|
| READ UNCOMMITTED | ロックをほぼ取得しない | なし(ダーティリードが発生) |
| READ COMMITTED | 読み取り時に短期間の共有ロックを取得 | ダーティリード |
| REPEATABLE READ | トランザクション終了まで共有ロックを保持 | ダーティリード・ノンリピータブルリード |
| SERIALIZABLE | 範囲ロックも取得し完全な直列化を保証 | 上記すべて+ファントムリード |
MySQLのInnoDBではデフォルト分離レベルがREPEATABLE READですが、MVCCによりロックなしで一貫した読み取りを実現しています。
MVCC(多版型同時実行制御)とロックの関係
現代のデータベース(PostgreSQL・MySQLのInnoDB・Oracleなど)では、MVCC(Multi-Version Concurrency Control:多版型同時実行制御)という技術が使われています。
MVCCとは、データの変更履歴(複数バージョン)を保持することで、読み取りトランザクションが書き込みトランザクションをブロックしない仕組みです。
「読み取りは書き込みをブロックせず、書き込みも読み取りをブロックしない」というMVCCの特性により、ロックの競合を大幅に減らして高い並行処理性能を実現しています。
MVCCを理解することで、「なぜ読み取り中に他の書き込みが進められるのか」「スナップショット読み取りとは何か」といった疑問も解決できます。
楽観的ロックと悲観的ロックの違い
続いては、アプリケーション設計でよく使われる「楽観的ロック」と「悲観的ロック」の概念について確認していきます。
悲観的ロックとは
悲観的ロック(Pessimistic Locking)とは、「データ競合は必ず起きる」という前提で、データにアクセスする際に最初からロックをかけて他のアクセスを排除する方式です。
SQLでは「SELECT … FOR UPDATE」を使うことで、読み取り時に排他ロックを取得できます。
在庫管理・座席予約など、競合が頻繁に発生する場面で使われます。
ロックを保持している間は他のトランザクションが待機するため、競合が少ない場面では無駄な待ち時間が発生するのがデメリットです。
楽観的ロックとは
楽観的ロック(Optimistic Locking)とは、「データ競合はめったに起きない」という前提で、ロックをかけずに処理を進め、更新時に競合が起きていないかを確認する方式です。
テーブルにバージョン番号(version列)やタイムスタンプ列を持たせ、更新時にWHERE version = 読み取り時のversionという条件を付けることで競合を検出します。
【楽観的ロックの実装イメージ】
1. SELECT id, stock, version FROM products WHERE id = 1;
→ stock=10, version=5 を取得
2. アプリ側で処理を行う(stock=9に変更予定)
3. UPDATE products SET stock=9, version=6 WHERE id=1 AND version=5;
→ 更新件数が0なら他のトランザクションが先に更新済み → エラー処理・再試行
競合が少ない場面では楽観的ロックのほうが高性能で、競合が多い場面では悲観的ロックのほうが確実な制御ができます。
まとめ
本記事では、トランザクションのロックの仕組み・共有ロックと排他ロック・行ロック・デッドロックの発生と対策・分離レベルとの関係・楽観的ロックと悲観的ロックの違いについて解説しました。
ロックはデータベースの並行処理において整合性を守るための根本的な仕組みであり、分離レベルやMVCCと密接に関係しています。
デッドロックは循環待ちによって発生し、ロック順序の統一やトランザクション短縮で防ぐことが重要です。
楽観的ロックと悲観的ロックを場面に応じて使い分けることが、高性能で安全なデータベース設計の鍵です。
本記事を参考に、ロックの仕組みへの理解を深め、データベース設計・開発のレベルアップにお役立てください。