第11章 トランザクションとロック

トランザクションとは

トランザクションとは、複数のデータベース操作をひとまとまりの作業として扱うための概念です。トランザクションは以下の4つの性質(ACID特性)を満たす必要があります。

  1. 原子性(Atomicity): トランザクション内のすべての操作が完全に成功するか、あるいは完全に失敗するかのどちらかでなければなりません。つまり、操作の一部だけが適用されることはありません。
  2. 一貫性(Consistency): トランザクションの実行前後でデータベースの一貫性が保たれます。すなわち、トランザクションがデータベースに適用されると、データベースは一貫性のある状態から新たな一貫性のある状態に遷移します。
  3. 独立性(Isolation): 同時に実行されるトランザクションは互いに独立しています。つまり、あるトランザクションが他のトランザクションに影響を与えることはありません。
  4. 持続性(Durability): トランザクションが一度成功すると、その結果は永続的にデータベースに保存されます。

MySQLでトランザクションを使用する基本的な方法は次の通りです。

  • トランザクションの開始: START TRANSACTION;コマンドまたはBEGIN;コマンドを使用します。
  • データベース操作の実行: INSERT、UPDATE、DELETEなどのコマンドを使用します。
  • トランザクションの確定: COMMIT;コマンドを使用して、トランザクション内のすべての操作をデータベースに適用します。
  • トランザクションの取消: ROLLBACK;コマンドを使用して、トランザクション内のすべての操作を取り消します。

下記はトランザクションを使った一例です。

START TRANSACTION;
UPDATE Account SET balance = balance - 100 WHERE id = 1;
UPDATE Account SET balance = balance + 100 WHERE id = 2;
COMMIT;

上記の例では、id=1の口座から100単位を引き出し、それをid=2の口座に預け入れるトランザクションを示しています。このトランザクションは原子性を持ち、すべての操作が成功すればCOMMITによって確定され、一部でも失敗すればROLLBACKによって全操作が取り消されます。

なお、MySQLではInnoDBとNDB Clusterという2つのストレージエンジンがトランザクションをサポートしています。他のストレージエンジン(例えばMyISAM)ではトランザクションを利用することはできません。使用するストレージエンジンを選ぶ際には、その機能と必要性を考慮する必要があります。

最後に、トランザクションの使用には注意が必要です。長時間にわたるトランザクションは、他のトランザクションが必要とするリソースをロックしてしまう可能性があります。これによりパフォーマンスが低下したり、デッドロック(2つ以上のトランザクションがお互いの終了を待ち合ってしまい、進行できなくなる状態)が発生する可能性があります。


ロックとは

データベースシステムにおけるロックは、同時に多数のトランザクション(データベース操作)が発生する場合に、データの整合性を維持するための重要な仕組みです。MySQLでは、データの整合性を保証するために、行レベルのロック、テーブルレベルのロック、それに加えてより高度なロック機構を提供しています。


ロックの種類

  • 行ロック (Row Locks): 行ロックは一つの行に対してのみロックをかけます。これは最も細かい粒度のロックで、他のトランザクションが同じ行を同時に変更することを防ぎます。
  • テーブルロック (Table Locks): テーブルロックはテーブル全体をロックします。これは粒度が大きく、他のトランザクションが同じテーブルに対して読み取りまたは書き込みを行うことを防ぎます。
  • 共有ロック (Shared Locks): 共有ロックは他のトランザクションが同じデータを読むことを許可するが、そのデータの変更を許可しないロックです。これは読み取り専用の操作で使用されます。
  • 排他ロック (Exclusive Locks): 排他ロックは他のトランザクションが同じデータを読むまたは書き込むことを許可しないロックです。これは書き込み操作で使用されます。
  • 意図ロック (Intention Locks): 意図ロックはトランザクションが特定のタイプのロックを獲得する意図を示すものです。これはテーブルレベルでのロックで、そのトランザクションがその後行レベルでロックを獲得するつもりであることを示します。
  • ギャップロック (Gap Locks): ギャップロックはインデックスレコード間の「ギャップ」をロックします。これは、そのギャップ内に新しい行が挿入されるのを防ぐために使用されます。
  • ネクストキーロック (Next-Key Locks): ネクストキーロックはレコードロックとギャップロックの組み合わせです。これはレコードとその「ギャップ」の両方をロックします。

これらのロックはそれぞれ独立していますが、相互に関連して作用します。例えば、行ロックはレコードロックと同じであり、ネクストキーロックはレコードロックとギャップロックを組み合わせたものです。また、共有ロックと排他ロックは同じデータに対する読み取りと書き込みを制御します。

これらのロックは全て、データの整合性を保つためにトランザクションがデータにアクセスする際の競合状況を管理するために使用されます。


行ロック

MySQLではInnoDBストレージエンジンが行ロックを提供し、読み取りと書き込みのロックの2つの主なタイプをサポートしています。

1. 共有ロック(Shared Locks、Sロック): 共有ロックは、トランザクションが特定の行を読み取ることを他のトランザクションが防ぐ形で働きます。つまり、あるトランザクションが特定の行にSロックを掛けた場合、他のトランザクションはその行を読むことはできますが、書き込みはできません。共有ロックは次のように使用します。

START TRANSACTION;
SELECT * FROM Orders WHERE OrderID = 1 FOR SHARE;

2.排他的ロック(Exclusive Locks、Xロック): 排他的ロックは、他のトランザクションが特定の行にアクセスすることを防ぎます。あるトランザクションが特定の行にXロックを掛けた場合、他のトランザクションはその行に対して読み取りも書き込みもできません。排他的ロックは次のように使用します。

START TRANSACTION;
SELECT * FROM Orders WHERE OrderID = 1 FOR UPDATE;

この2つのロックタイプを理解するための一例として、銀行の口座間送金が考えられます。AさんがBさんに送金するとき、システムは次の手順を踏みます。

  • Aさんの口座に排他的ロック(Xロック)を掛けます(他のトランザクションが同時にAさんの口座を更新できないようにします)。
  • Bさんの口座に排他的ロック(Xロック)を掛けます(他のトランザクションが同時にBさんの口座を更新できないようにします)。
  • Aさんの口座から送金額を減算し、Bさんの口座に送金額を加算します。
  • すべての更新が成功したら、ロックを解除します。

このように、MySQLの行ロック機能を使用することで、データの一貫性を保ちつつ、データベースへの同時アクセスを適切に管理することができます。


トランザクション分離レベル

トランザクション分離レベルは、同時に実行される複数のトランザクションがどの程度互いに影響を与えるかを制御するための設定です。MySQLでは、次の4つの分離レベルが用意されています。

  • READ UNCOMMITTED (未コミット読み取り): 他のトランザクションがまだコミットしていないデータを読み取ることが可能です。これにより、「ダーティリード」と呼ばれる現象が生じ、他のトランザクションがロールバックした場合でも、未コミットのデータを読み取ることができます。
  • READ COMMITTED (コミット済み読み取り): 他のトランザクションがコミットしたデータのみを読み取ることが可能です。これにより、ダーティリードは防止されますが、同一トランザクション内での同じクエリの結果が異なる「ノンリピータブルリード」が生じる可能性があります。
  • REPEATABLE READ (リピータブルリード): トランザクションが開始された時点のスナップショットを保持し、トランザクションの実行中はそのスナップショットからデータを読み取ります。これにより、ノンリピータブルリードは防止されますが、同一トランザクション内で新たに挿入された行が見えない「ファントムリード」が生じる可能性があります。MySQLのInnoDBストレージエンジンのデフォルトの分離レベルはこれです。
  • SERIALIZABLE (直列化): トランザクションを完全に分離し、同時に実行されるトランザクションが全く互いに影響を与えないようにします。これにより、ダーティリード、ノンリピータブルリード、そしてファントムリードを全て防止しますが、パフォーマンスに影響を及ぼす可能性があります。

これらの分離レベルは、データの一貫性とパフォーマンスのトレードオフを制御するために使用されます。


デッドロック

デッドロックとは、複数のトランザクションが互いに他のトランザクションが保持するロックを待つことで、どのトランザクションも進行できなくなってしまう状態のことを指します。この状態は"循環待機"とも呼ばれます。

MySQLのInnoDBストレージエンジンは、デッドロックを自動的に検出して解決する機能を持っています。しかし、その解決方法は一つのトランザクションを"犠牲にする"ことで、他のトランザクションが進行できるようにするというものです。この"犠牲にされる"トランザクションはロールバックされ、アプリケーションはその失敗を検出して適切に対応する必要があります。

デッドロックの一例として、以下のような状況を考えてみましょう。

  • トランザクション1がレコードAのロックを取得します。
  • トランザクション2がレコードBのロックを取得します。
  • トランザクション1がレコードBのロックを取得しようとしますが、それは既にトランザクション2によってロックされているため、待機状態になります。
  • 同様に、トランザクション2がレコードAのロックを取得しようとしますが、それは既にトランザクション1によってロックされているため、待機状態になります。

この状況はデッドロックであり、どちらのトランザクションも他方がロックを解放するのを待つしかなく、それが永遠に起こらないために進行できなくなります。

デッドロックを避けるための一般的なアプローチの一つは、アプリケーションが必要とする全てのロックを一度に取得すること、または常に同じ順序でロックを取得することです。この方法を用いると、上記の例のような循環待機の状況を回避することができます。ただし、このような方法はアプリケーションの設計やパフォーマンスに影響を及ぼす可能性がありますので、注意が必要です。