トランザクションは、複数のデータベース操作を「全部成功」か「全部なかったことにする」かのどちらかにまとめる仕組みです。銀行の口座振替で片方の残高だけが減る事故を防ぐのが役割で、その安全性を支えるのがACID特性です。そしてトランザクション分離レベルとは、同時に走る複数のトランザクションが互いの途中経過をどこまで見てよいかを段階で決める設定を指します。この記事では、commitとrollbackの基本動作から、READ COMMITTEDなど分離レベル4種と並行処理で起きる読み取り異常、分離レベルを確認・変更するSQLとアプリ側の指定方法、共有ロックと排他ロックによる排他制御、そしてトランザクション境界の設計判断や分散トランザクションの選び方まで、業務システムを実装・選定する立場で整理します。PostgreSQL・MySQL・SQL Server・Oracleで既定の挙動がどう違うかも、具体的に押さえる対象です。
まとめ:トランザクションと分離レベルの定義と実装で押さえる判断の要点
トランザクションとは、分けられない1つの処理単位として複数の操作をまとめ、成功時にcommitで確定し、失敗時にrollbackで開始前の状態へ戻す仕組みです。この「全か無か」を保証する性質がACID特性のA(原子性)で、残る一貫性・分離性・永続性と合わせてデータの信頼性を支えます。
実装で判断が分かれるのは分離レベルの選択です。分離レベルは、ACIDのI(分離性)をどこまで厳密に守るかを利用者が選べるようにしたつまみで、標準SQLは READ UNCOMMITTED・READ COMMITTED・REPEATABLE READ・SERIALIZABLE の4段階を定めています。既定でどこまで並行処理を許すかはDB製品によって異なり、PostgreSQL・SQL Server・Oracleは READ COMMITTED、MySQLのInnoDBは REPEATABLE READ が初期値です(いずれも2026年9月時点の主要系列)。緩い分離レベルは読み取り異常と引き換えに速度を稼ぎ、SERIALIZABLEは異常を消す代わりにロック競合とやり直しが増えます。
結論として、実務ではまず「トランザクションを短く保つ」ことを最優先に設計します。分離レベルは既定値を出発点にし、上げるのは「同一トランザクション内で読んだ値を前提に書く処理」だけに絞ります。上げるならシリアライズ失敗(PostgreSQLではSQLSTATE 40001)を受けてやり直すリトライ処理とセットで入れる設計が必要です。ユーザーの入力待ちを挟んだ長時間トランザクション、1件ずつcommitするループ、外部API呼び出しをトランザクション内に閉じ込める作りは、ロック保持とデッドロックの主因になるため避けます。整合性が金銭や在庫に直結する基幹システムでは、この境界設計を最初から誤らないことが後戻りコストを抑える鍵になります。
トランザクションの定義とcommit・rollbackの基本動作
トランザクションは、データベースに対する一連の読み書きを「これ以上分割できない1単位」として扱う概念です。単位の途中で処理が止まっても中途半端な状態を残さない、という約束がすべての出発点になります。
口座振替の具体例で理解するトランザクションの原子的な処理単位
A口座からB口座へ1万円を移す処理は、「Aから1万円引く」「Bへ1万円足す」という2つの更新から成ります。片方だけが実行されて障害で止まると、1万円が消えるか二重になる事故です。この2操作を1つのトランザクションにまとめると、両方成功して初めて結果が残り、途中で失敗すれば両方が取り消されます。
この「全部やる、さもなくば何もしない」という単位化が、トランザクションの本質です。1件の更新に見える処理でも、裏でインデックス更新やトリガが走るなら、それらもまとめて1単位として扱われます。トランザクションを管理する主体はDBMS本体であり、その全体像はDBMSとは?機能・種類・RDBMS製品の選び方で整理しています。
BEGIN・COMMIT・ROLLBACKで区切るトランザクションの明示的な境界
SQLでは、トランザクションの開始をBEGIN(またはSTART TRANSACTION)で宣言し、確定をCOMMIT、取り消しをROLLBACKで指示します。commitした時点で結果がディスクに永続化され、他の利用者からも見えるようになります。口座振替をSQLで書くと次の形です。
BEGIN;
UPDATE accounts SET balance = balance - 10000 WHERE id = 'A';
UPDATE accounts SET balance = balance + 10000 WHERE id = 'B';
-- 残高がマイナスになる等の業務エラーを検知したら ROLLBACK; で取り消す
COMMIT;
明示的にBEGINを書かない単発のSQLは、多くのRDBMSで「自動コミット(autocommit)」として1文ごとに即座に確定します。複数文をまとめたいときだけ境界を自分で宣言する、という使い分けです。多くのORMやフレームワークは、トランザクションブロックの内側で自動的にautocommitを無効化するため、通常は個別に切り替える必要はありません。長い処理の途中に戻り先を作るSAVEPOINTを置けば、トランザクション全体ではなく一部だけをrollbackすることもできます。COMMITやROLLBACKがどの文の種類にあたるかはSQLとは?文の種類と実行順序で扱う制御文(TCL)に位置づけられます。
ACID特性が支えるデータ整合性とトランザクション処理の信頼性
ACIDは、トランザクションが満たすべき4つの性質の頭文字です。原子性(Atomicity)、一貫性(Consistency)、分離性(Isolation)、永続性(Durability)を指し、これらが揃って初めて「データベースを信頼して業務を載せられる」状態になります。
原子性・一貫性・分離性・永続性というACIDの4つの構成要素
それぞれの性質が守るものは次のとおりです。実務では、原子性と永続性は多くのRDBMSが標準で守り、分離性だけが後述の分離レベルで調整可能、と押さえると全体像がつかめます。
| 特性 | 保証する内容 | 破れたときの例 |
|---|---|---|
| 原子性 | 全実行か全取消 | 振替で片方だけ更新 |
| 一貫性 | 制約を常に満たす | 残高がマイナス値に |
| 分離性 | 同時実行が干渉しない | 未確定データを他が参照 |
| 永続性 | commit後は消えない | 停電で確定分が消失 |
一貫性は、原子性・分離性・永続性とアプリ側の制約定義が守られた結果として保たれる性質で、単独の仕組みというより他の3つの帰結にあたります。外部キーやCHECK制約をDB側に置くほど、この一貫性をアプリの実装ミスから守りやすくなる仕組みです。制約を置きやすいテーブル構造の作り方はデータベース正規化の手順と非正規化の判断で扱っています。
WALによるログ先行書き込みがcommit後の永続性を担保する仕組み
永続性は、commitしたデータが障害後も残ることを指します。多くのRDBMSは、データ本体を更新する前に変更内容をログへ先に書く「WAL(Write-Ahead Logging)」でこれを実現する仕組みです。ログさえディスクに残っていれば、クラッシュ後の再起動時にログを再適用して確定済みの結果を復元できます。ログを先に固定する順序制約や、fsyncを緩めたときに失われる範囲はライトアヘッドログ(WAL)の仕組みと運用で詳しく扱っています。
この仕組みは、commit時にログの書き込み完了を待つため、ディスクI/Oの速さが応答時間を左右します。大量の小さなトランザクションを高頻度でさばくOLTP(オンライントランザクション処理)では、この書き込みがボトルネックになりやすく、処理方式の特徴はOLTPとは|OLAP・DWHとの違いとHTAP・AWS DB選定を比較解説で解説しています。
トランザクション分離レベルとは何か:4段階の定義と並行処理で生じる読み取り異常
複数のトランザクションが同時に走ると、片方の未確定・途中の変更をもう片方が見てしまう「読み取り異常」が起こり得ます。どこまで許容するかを段階で定義したものが分離レベルです。すべてのトランザクションを1本ずつ順番に実行すれば異常は起きませんが、それでは同時に数百の注文をさばけません。分離レベルは「どの異常までなら許して並行性を取るか」という取引の目盛りであり、標準SQLは4段階を規定します。
ダーティリード・ノンリピータブルリード・ファントムの3つの異常
分離が緩いほど発生し得る異常が増えます。代表的な3つの異常は次のように区別します。
- ダーティリード:他のトランザクションが未commitの変更を読んでしまう。相手がrollbackすれば、存在しない値を読んだことになる。
- ノンリピータブルリード:同じ行を2回読むと、間に他がcommitした更新で値が変わっている。
- ファントムリード:同じ条件で検索すると、間に他が挿入した行が2回目に増えている。
ダーティリードは金額の集計などで致命的になりやすく、実務で許容する場面はほとんどありません。ノンリピータブルとファントムは、同一トランザクション内で繰り返し集計する処理でだけ問題になります。
2つのセッションで再現するノンリピータブルリードとREAD COMMITTEDの挙動
言葉だけでは違いがつかみにくいので、PostgreSQLの既定である READ COMMITTED で、ターミナルを2つ開いて再現してみます。セッションAが同じ行を2回読む間に、セッションBが更新してcommitします。
-- 準備(どちらかのセッションで1回だけ実行)
CREATE TABLE stock (item_id int PRIMARY KEY, qty int NOT NULL);
INSERT INTO stock VALUES (1, 10);
-- セッションA
BEGIN; -- 既定の READ COMMITTED
SELECT qty FROM stock WHERE item_id = 1; -- 10
-- セッションB
BEGIN;
UPDATE stock SET qty = 7 WHERE item_id = 1;
COMMIT;
-- セッションA(同じトランザクションのまま)
SELECT qty FROM stock WHERE item_id = 1; -- 7 に変わる=ノンリピータブルリード
COMMIT;
セッションAの最初のBEGINをBEGIN ISOLATION LEVEL REPEATABLE READ;に変えて同じ手順を踏むと、2回目のSELECTも10を返します。REPEATABLE READでは、トランザクションの開始時点のスナップショットを最後まで見続けるためです。一方で、セッションBのcommit前にAが読んでも7は見えないので、ダーティリードはどちらのレベルでも起きません。
標準SQLの4つの分離レベルと各段階で許容される読み取り異常
分離レベルと、その段階で起こり得る異常の関係を整理します。下に行くほど厳格になり、異常は消えますが並行性は落ちます。
| 分離レベル | ダーティ | ノンリピータブル | ファントム |
|---|---|---|---|
| READ UNCOMMITTED | 起こる | 起こる | 起こる |
| READ COMMITTED | 防ぐ | 起こる | 起こる |
| REPEATABLE READ | 防ぐ | 防ぐ | 製品による |
| SERIALIZABLE | 防ぐ | 防ぐ | 防ぐ |
この表は標準SQLが「起こってもよい」とする上限で、製品はそれより厳しく実装してかまいません。PostgreSQLの公式ドキュメント(13.2 Transaction Isolation)は、READ UNCOMMITTEDを指定してもREAD COMMITTEDとして動き、REPEATABLE READでもファントムリードは起きないと明記しています。内部で実装されている分離レベルは実質3種類という扱いです。MySQLのInnoDBも REPEATABLE READ でネクストキーロックによりファントムをほぼ抑えます。分離レベルはSET TRANSACTION ISOLATION LEVELで切り替えられますが、既定値のまま運用するケースが大半です。これらの分離レベルをロックではなく行の版で実現する仕組みは、MVCC(多版型同時実行制御)の仕組みを実装目線で解説した記事で扱っています。
標準SQLの表に載らないロストアップデートと書き込みスキューの2つの異常
上の表で「防ぐ」が並んでいても、安心できない異常が2つあります。どちらも読んだ値を前提にアプリが書き戻す処理で起き、業務システムの不具合として実際に表面化しやすいのはこちらです。
- ロストアップデート(更新の消失):2つのトランザクションが同じ在庫数10を読み、それぞれ「読んだ値−1」を書き戻すと、2個売れたのに在庫は9になる。READ COMMITTEDでは防げない。
- 書き込みスキュー:「当直医は最低1人」という規則で、2人の医師が同時に「もう1人いるから外れる」と判断して別々の行を更新し、当直が0人になる。互いに違う行を書くため、行ロックの衝突として検出されない。
PostgreSQLでは、REPEATABLE READで同じ行への同時更新を検出するとエラーでトランザクションを中断させるため、ロストアップデートは防げます。ただし書き込みスキューは REPEATABLE READ でも起こり得て、公式ドキュメントの表でも「Serialization Anomaly」列が Possible のままです。これを止められるのは SERIALIZABLE だけです。MySQLのInnoDBは REPEATABLE READ でもロストアップデートを検知しないため、SELECT ... FOR UPDATEで読む時点から行ロックを取るか、UPDATE stock SET qty = qty - 1 WHERE item_id = 1 AND qty > 0のように読みと書きを1文にまとめて防ぎます。
PostgreSQL・MySQL・SQL Server・Oracleの既定分離レベル
同じSQLでも、DB製品のデフォルト分離レベルが違うと挙動が変わります。移行やマルチDB構成で見落とすと不具合の原因になります(いずれも2026年9月時点の主要系列の既定値。PostgreSQLは公式のバージョン方針ページで18系が最新の安定版)。
| 製品 | 既定の分離レベル | 特徴 |
|---|---|---|
| PostgreSQL | READ COMMITTED | MVCCで読取を非ブロック |
| MySQL(InnoDB) | REPEATABLE READ | ネクストキーロック採用 |
| SQL Server | READ COMMITTED | 既定はロック方式 |
| Azure SQL Database | READ COMMITTED | 行バージョン方式が既定 |
| Oracle | READ COMMITTED | SERIALIZABLE選択可 |
PostgreSQLとOracleは既定でノンリピータブルリードが起こり得るのに対し、MySQLは既定でそこまで防ぎます。「開発環境のMySQLでは再現しないのに本番のPostgreSQLで整合性がずれる」といった事象は、この既定値の差が原因のことがあります。両製品の差をほかの観点も含めて比べるならPostgreSQLとMySQLの違いを徹底比較した記事が参考になり、MySQL側の前提はMySQLとは?特徴とバージョン選定も参照してください。
SQL Serverは名前が同じ READ COMMITTED でも、中身が設定で変わる点に注意が要ります。MicrosoftのSET TRANSACTION ISOLATION LEVELのドキュメントによると、SQL Serverではデータベースオプション READ_COMMITTED_SNAPSHOT が既定でOFFで、読み取りは共有ロックで他の更新を待ちます。Azure SQL DatabaseとMicrosoft FabricのSQL databaseでは既定がONで、ロックを取らず文の開始時点の版を読む仕組みです。開発をAzure SQL Database、本番をオンプレミスのSQL Serverで動かすと、同じコードでもブロッキングの出方が変わります。
Oracleが用意する分離レベルは READ COMMITTED・SERIALIZABLE・READ ONLY の3種類で、Oracle AI Database 26系のConceptsドキュメントに整理されています。SERIALIZABLEのトランザクションが、開始後に他がcommitした行を更新しようとするとORA-08177で失敗するため、PostgreSQLの40001と同じくアプリ側のやり直しが前提になります。
分離レベルを確認・変更するSQLとアプリ側の設定を製品別のコード例で試す
分離レベルの理屈がわかったら、次は手元のDBで現在値を確かめ、必要な処理だけ切り替えます。設定には「その1トランザクションだけ」「接続(セッション)単位」「データベース全体の既定」の3つの粒度があり、影響範囲の小さい順に検討するのが安全です。
PostgreSQLとMySQLで現在の分離レベルを確認して切り替えるSQL
PostgreSQLではSHOWで現在値を確認し、SET TRANSACTIONのリファレンスにある構文で切り替えます。SET TRANSACTIONはトランザクション内の最初の問い合わせより前に実行する必要があります。
-- PostgreSQL 18系
SHOW transaction_isolation; -- 実行中トランザクションの分離レベル
SHOW default_transaction_isolation; -- 新しいトランザクションの既定値
-- このトランザクションだけ変える(最も影響が小さい)
BEGIN ISOLATION LEVEL REPEATABLE READ;
-- 処理
COMMIT;
-- データベース全体の既定を変える(新しい接続から有効)
ALTER DATABASE app_db SET default_transaction_isolation = 'repeatable read';
既定値を決めるパラメータ default_transaction_isolation の扱いはClient Connection Defaultsの章に載っています。データベース全体の既定を変えると、そのDBに接続する全アプリの挙動が変わるため、個別処理で足りるなら1トランザクション単位の指定にとどめます。MySQLで確認・変更する際に使うSQLは次のとおりです。
-- MySQL 8.0以降(変数名は transaction_isolation)
SELECT @@transaction_isolation, @@global.transaction_isolation;
-- 次の1トランザクションだけ変える
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
-- 処理
COMMIT;
-- この接続の間ずっと変える
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
MySQLでSESSIONもGLOBALも付けないSET TRANSACTIONは、次に始まる1トランザクションにだけ効きます。PostgreSQLとは書き方が似ていて効く範囲が違うため、移植時の取り違えに気をつけます。
SQL Serverのロック方式と行バージョン方式を切り替える設定
SQL Serverでは、分離レベル名とは別に、データベースオプションで READ COMMITTED の実装方式が変わります。現在のセッションの分離レベルはDBCC USEROPTIONSで、データベースの設定はsys.databasesで確認できます。
-- 現在のセッションの設定(isolation level の行を見る)
DBCC USEROPTIONS;
-- データベースごとの READ_COMMITTED_SNAPSHOT と SNAPSHOT の許可状態
SELECT name, is_read_committed_snapshot_on, snapshot_isolation_state_desc
FROM sys.databases;
-- READ COMMITTED を行バージョン方式に切り替える(他の接続が無い時間帯に実施)
ALTER DATABASE app_db SET READ_COMMITTED_SNAPSHOT ON WITH ROLLBACK IMMEDIATE;
-- SNAPSHOT 分離レベルを使えるようにする
ALTER DATABASE app_db SET ALLOW_SNAPSHOT_ISOLATION ON;
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
行バージョン方式にすると読み取りが更新を待たなくなる代わりに、古い版を保持するためにtempdbの使用量が増えます。WITH ROLLBACK IMMEDIATEは実行中のトランザクションを強制的にrollbackさせるため、本番での実行は保守時間帯に限定する運用です。SNAPSHOT分離レベルは ALLOW_SNAPSHOT_ISOLATION をONにしてからでないと開始できない点も、前述のMicrosoftのドキュメントに明記されています。
Spring BootやPythonのコードで分離レベルを指定しリトライする実装
アプリ側では、フレームワークの宣言で分離レベルを指定するのが一般的です。Spring Frameworkの@Transactionalのドキュメントによると、既定の isolation は ISOLATION_DEFAULT で、これは「DB側の既定値に従う」という意味です。つまり何も書かなければ、PostgreSQLならREAD COMMITTED、MySQLならREPEATABLE READで動きます。
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OnCallService {
// 書き込みスキューを防ぎたい処理だけ SERIALIZABLE にする
@Transactional(isolation = Isolation.SERIALIZABLE)
public void leaveOnCall(long doctorId, long shiftId) {
long onCall = repo.countOnCall(shiftId);
if (onCall < 2) {
throw new IllegalStateException("当直が0人になるため外れられません");
}
repo.setOffCall(doctorId, shiftId);
}
}
SERIALIZABLEやPostgreSQLのREPEATABLE READを使う処理は、衝突時にDBがトランザクションを中断させるため、呼び出し側でやり直す仕組みが必須です。PostgreSQLの13.5 Serialization Failure Handlingは、シリアライズ失敗が常にSQLSTATE 40001を返し、デッドロック(40P01)もリトライ対象にしてよいと説明しています。Python(psycopg 3系)で書くと次のようになります。
import time
import psycopg
from psycopg import errors
def leave_on_call(dsn, doctor_id, shift_id, retries=3):
with psycopg.connect(dsn, autocommit=True) as conn:
for attempt in range(retries):
try:
with conn.transaction():
conn.execute("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE")
n = conn.execute(
"SELECT count(*) FROM on_call WHERE shift_id = %s AND active",
(shift_id,),
).fetchone()[0]
if n < 2:
raise ValueError("当直が0人になるため外れられません")
conn.execute(
"UPDATE on_call SET active = false WHERE doctor_id = %s AND shift_id = %s",
(doctor_id, shift_id),
)
return
except (errors.SerializationFailure, errors.DeadlockDetected):
# 40001 と 40P01 は最初からやり直す。回数を超えたら呼び出し元へ返す
if attempt == retries - 1:
raise
time.sleep(0.05 * (2 ** attempt))
リトライでは、トランザクションの中身を丸ごと最初から実行し直すことが肝心です。失敗した文だけを再実行しても、前提にしたSELECTの結果が古いままなので意味がありません。ORMを経由する場合も同じで、どの層でトランザクションが開始・終了しているかを把握しておく必要があります。ORMがSQLとトランザクションをどう肩代わりするかはORMとは?O/Rマッパーの仕組み・メリットとデメリットで整理しています。
排他制御とロックの基本の仕組みと共有ロック・排他ロックの違い
分離性を実現する土台が排他制御です。同じデータへの同時アクセスを、ロックによって順番待ちさせたり、読み取り専用の複製を見せたりして干渉を防ぎます。ロックの粒度と種類を理解すると、性能と整合性のトレードオフが見えてきます。
共有ロックと排他ロックの両立可否と行・テーブル単位のロック粒度
ロックには大きく2種類あります。読み取り用の共有ロックは複数のトランザクションが同時に取得でき、書き込み用の排他ロックは1つしか取得できず、共有ロックとも両立しません。この非両立の関係が「読んでいる間は書けない、書いている間は読めない」を生みます。なお、どの分離レベルを選んでも、更新するデータには排他ロックがかかりトランザクション終了まで保持されます。分離レベルで変わるのは主に読み取り側の振る舞いです。
ロックをかける単位(粒度)は、行・ページ・テーブルと段階があります。行ロックは並行性が高い一方でロック管理のコストがかかり、テーブルロックは単純ですが同時実行を大きく制限する特性です。1件ずつ狙って更新するなら行ロック、集計や一括洗い替えならテーブル単位、という使い分けになります。
競合頻度で判断する楽観ロックと悲観ロックの使い分けと回避方針
アプリ側の設計判断として、ロックをかけてから更新する悲観ロックと、更新直前にバージョンの不一致を検出する楽観ロックがあります。競合が稀な画面編集では楽観ロック、在庫引き当てのように競合が前提の処理では悲観ロックが向く、という住み分けです。前章のロストアップデートは、分離レベルを上げなくてもこのどちらかで防げます。両者の実装差はDB別に楽観ロックと悲観ロックとは?違い・使い分けで詳述しています。
複数のトランザクションが互いのロック解放を待ち合って進めなくなる状態がデッドロックです。更新するテーブルやレコードの順序をアプリ全体で揃えるだけで、多くのデッドロックは避けられます。検出と解消の具体策はデッドロックとは?原因・検出・予防策で扱っています。
トランザクション境界の設計判断と実装で避けたいアンチパターン
トランザクションの正しさは、ACIDの理解よりも「どこから始めてどこで終えるか」という境界設計で決まります。ここでは、実装現場で繰り返し見かける失敗と、その回避の判断基準を言い切ります。
長時間トランザクションと外部API呼び出しを境界に含めない設計
最も避けるべきは、ユーザーの確認待ちや外部APIの応答待ちをトランザクションの内側に入れることです。ロックを保持したまま人間や外部システムを待つことになり、その間ほかの処理が止まり、タイムアウトやデッドロックを誘発します。MVCCを採るDBでは、長く開いたトランザクションが古い行の版の回収も妨げるため、テーブルの肥大化という別の問題も招きます。
判断基準はシンプルです。トランザクションはミリ秒〜数百ミリ秒で閉じることを目標にし、外部API呼び出しは境界の外に出します。決済APIのように外部呼び出しと整合を取りたい場合は、いったんローカルに「処理中」状態を確定させ、外部応答は別トランザクションで反映する二段構えにします。ここを守れないなら、その処理は1つのトランザクションで完結させるべきではありません。
1件ずつcommitするループと一括処理の速度と安全のトレードオフ
大量データを1件ずつBEGIN・COMMITするループは、commitごとにWALの書き込み待ちが発生し、処理が桁違いに遅くなります。一方で数十万件を1トランザクションにまとめると、ロック保持が長くなり、失敗時のrollbackも重くなります。
実務での落としどころは、数百〜数千件ごとにcommitする「バッチコミット」です。この単位なら書き込み回数を抑えつつ、ロック保持時間とrollbackの範囲も限定できます。件数の適切な区切りは行サイズとロック競合の度合いで変わるため、本番相当のデータ量で計測して決めるのが確実です。データ更新の実行単位そのものの考え方はクエリとは?実行の仕組みと高速化も参考になります。
分離レベルを上げるかロックで守るかを処理の性質で決める判断基準
整合性の不具合を見つけたとき、反射的にデータベース全体の分離レベルを上げるのは悪手です。全処理が遅くなり、リトライ前提のエラーがあちこちで出るようになります。処理の性質ごとに、次の基準で手段を選びます。
| 処理の性質 | 選ぶ手段 | 理由 |
|---|---|---|
| 1行の加減算 | 1文のUPDATE | 読みと書きが原子的 |
| 同じ行を奪い合う | SELECT FOR UPDATE | 待たせて確実に直列化 |
| 競合が稀な画面編集 | 楽観ロック | 待ちが発生しない |
| 複数行の条件で判断 | SERIALIZABLE | 書き込みスキューを検出 |
| 集計や帳票の読み取り | REPEATABLE READ | 一時点の値で揃う |
採用条件を言い切ると、SERIALIZABLEを選ぶのは「複数行を読んだ結果から、別の行を書くかどうかを決める処理」で、しかもリトライを実装できる場合に限ります。該当するのは、当直・座席の割り当てや、残枠の合計を見て予約を受ける処理です。逆に、1行の在庫を減らすだけの処理でSERIALIZABLEを使うのは見送ります。1文のUPDATEか行ロックで足り、衝突時のやり直しコストだけが増えるからです。帳票のように読み取り専用で一時点の値を揃えたい処理は、REPEATABLE READやOracleのREAD ONLYで読むと、途中で他が更新しても合計が食い違いません。
基幹システムで整合性要件からトランザクション設計を逆算する判断
会計・受発注・在庫のような基幹システムでは、「どのデータが同時に正しくなければ業務が破綻するか」を先に定義し、その単位をトランザクション境界にします。逆に、通知の送信やログの記録のように多少ずれても業務が壊れないものは、あえて境界の外に出して本体の処理を軽くします。
この逆算を誤ると、後からロック競合や不整合が頻発し、作り直しに大きなコストが生じかねません。既存システムでも、DB移行で既定の分離レベルが変わった(MySQLからPostgreSQLへ移してREPEATABLE READからREAD COMMITTEDになった等)のを見落とし、移行後に集計のずれが出る例があります。整合性要件からのテーブル設計や、分離レベルの差を踏まえたDB移行を外部に相談する場合は、データベース設計・移行支援で承っています。
分散トランザクションで2相コミットとSagaパターンを選ぶ基準
マイクロサービスや複数DBにまたがる処理では、1つのトランザクションで全体を確定できません。複数のリソースをまとめて整合させる仕組みが分散トランザクションで、代表的な2つの手法をどう選ぶかを整理します。
2相コミットの準備・コミット2フェーズの仕組みと同期整合の限界
2相コミット(2PC)は、調整役が全参加者に「準備できたか」を問い(準備フェーズ)、全員がOKなら「確定せよ」と指示する(コミットフェーズ)方式です。全リソースを同期的に整合させられる反面、準備完了後に調整役が落ちると参加者がロックを抱えたまま宙づりになるブロッキング問題を抱えます。
この特性から、2PCはネットワークが安定し参加者が少ない構成、例えば同一データセンター内の複数DBをまとめる場面に向きます。サービス数が多くネットワーク遅延が読めないマイクロサービスでは、待ち時間と障害時のリスクが見合わなくなる点が難点です。異なる種類のDBをまたいでACIDを実現する製品側のアプローチもあり、その一例はScalarDB Clusterの構造と導入を解説した記事で扱っています。
Sagaパターンと補償トランザクションによる結果整合性の実現
Sagaは、処理を複数のローカルトランザクションに分割し、途中で失敗したら「補償トランザクション」で前の処理を打ち消していく方式です。全体を一瞬で整合させる代わりに、最終的に整合する「結果整合性」を受け入れます。ロックを長く持たないためスケールしやすい一方、補償ロジックを1ステップごとに自前で設計する負担を伴う方式です。各ステップの途中状態は他のサービスから見えるため、Saga全体としては分離性が無い、という点も設計時に織り込みます。
単一DBで完結させる選択肢も含めた分散処理の採否を分ける基準
ここは判断を言い切ります。分散トランザクションは、可能な限り採用しないのが最善です。サービスを分けたい理由が組織や規模の都合であって整合性要件でないなら、まず同一DB内のトランザクションで完結する設計に寄せます。単一DBで足りるなら、2PCもSagaも要りません。
それでも分散が避けられない場合は、強い整合が必須で参加者が少数なら2PC、参加者が多くスケールと可用性を優先するならSaga、と切り分けます。「なんとなくマイクロサービスだから2PC」という選び方は、ブロッキングと運用の複雑さを持ち込むだけなので採用しません。
よくある質問
トランザクションと分離レベルの実装でつまずきやすい点を、よく寄せられる疑問の形で補足します。
トランザクション分離レベルとは簡単に言うと何ですか?
同時に動いている他のトランザクションの途中経過や確定結果を、自分のトランザクションからどこまで見えてよいかを決める設定です。標準SQLは READ UNCOMMITTED・READ COMMITTED・REPEATABLE READ・SERIALIZABLE の4段階を定め、後ろほど読み取り異常が減る代わりに並行性が落ちます。ACIDのうち分離性(Isolation)の強さを調整するつまみと捉えると整理しやすくなります。
トランザクションとロックの違いは何ですか?
トランザクションは「複数操作を1単位にまとめ、全か無かで確定する」という論理的な枠組みです。ロックは、その分離性を実現するための下位の仕組みで、同じデータへの同時アクセスを順番待ちさせる技術です。トランザクションが目的、ロックがそれを支える手段という関係になります。
commitとrollbackはいつ実行すればよいですか?
一連の更新がすべて成功し、業務として整合が取れた時点でcommitします。途中で例外が発生したり、業務ルールに反する状態が見つかった場合はrollbackして開始前に戻します。アプリのフレームワークでは、処理が正常終了したら自動commit、例外送出時に自動rollbackする形が一般的です。
分離レベルは変更したほうがよいですか?
多くのケースでは既定値のままで問題ありません。変更を検討するのは、同一トランザクション内で同じ集計を複数回行い結果を揃えたい場合や、複数行の条件を見て書き込む処理で書き込みスキューを防ぎたい場合(より厳格へ)、参照専用で速度を優先したい場合(より緩やかへ)に限られます。変えるときもデータベース全体ではなく該当トランザクションだけに指定し、厳格にするならリトライ処理を同時に実装します。既定値はDB製品で異なるため、まずSHOW transaction_isolationやSELECT @@transaction_isolationで自分の使うDBの既定を確認する手順が必要です。
NoSQLにトランザクションはありますか?
製品による、というのが実情です。かつてはNoSQLはトランザクション非対応とされましたが、現在はドキュメントDBの複数ドキュメント更新など、複数操作にまたがるトランザクションを備える製品が増えています。ただしRDBMSほど強い保証を全機能で提供するとは限らないため、採用時は対象操作の保証範囲と分離の強さを製品ドキュメントで確認します。
関連記事
- MVCCとは?多版型同時実行制御の仕組みと過去版の回収を実装目線で解説:分離レベルを行の版で実現する仕組みを掘り下げています。
- DBMSとは?機能・種類・RDBMS製品の選び方:トランザクションを管理する主体であるDBMSの全体像を解説しています。
- SQLとは?文の種類と実行順序:COMMITやROLLBACKが属する制御文(TCL)の位置づけを整理しています。
- 楽観ロックと悲観ロックとは?違い・使い分け:排他制御の実装をDB別に掘り下げた記事です。
- デッドロックとは?原因・検出・予防策:ロック競合が行き詰まるデッドロックの具体策を扱います。
- OLTPとは|OLAP・DWHとの違いとHTAP・AWS DB選定を比較解説:トランザクションを高頻度でさばく処理方式の解説です。