【Oracle】ORA-08177の原因と解決方法|can’t serialize access for this transaction・SERIALIZABLE分離レベルの競合対策

【Oracle】ORA-08177の原因と解決方法|can’t serialize access for this transaction・SERIALIZABLE分離レベルの競合対策 Oracle

ORA-08177: can't serialize access for this transaction は、Oracleで SERIALIZABLE 分離レベルを使っているトランザクションが、他トランザクションの更新と整合しなくなった時に発生するエラーです。ロック待ちでずっと止まっている状態ではなく、Oracleが「このトランザクションを直列実行したものとして安全に続行できない」と判断した状態です。

Oracle公式のエラーメッセージでは、直列化できないアクセスが発生したことが示されています。またOracle Database Conceptsでは、serializable transaction がORA-08177で失敗した場合、アプリケーション側で対応が必要になることが説明されています。

先に結論
ORA-08177は、単純な構文ミスや権限不足ではありません。SERIALIZABLE 分離レベルで並行更新や同時実行が発生し、トランザクションの整合性を保ったまま続行できない時に出ます。基本方針は、短いトランザクションへ分ける、競合しやすい処理を直列化する、必要ならリトライする、分離レベルを見直す、の順で考えます。
スポンサーリンク

ORA-08177とは

ORA-08177は、主に SET TRANSACTION ISOLATION LEVEL SERIALIZABLE や、アプリケーション側でSERIALIZABLE相当の分離レベルを指定している場合に出ます。Oracleの通常の分離レベルは READ COMMITTED です。READ COMMITTEDでは各SQLの実行時点で整合性のある結果を見ますが、SERIALIZABLEではトランザクション開始時点の一貫した見え方を保とうとします。

そのため、長いトランザクションの途中で別セッションが同じ行や関係するデータを更新してコミットすると、後続の更新や参照でORA-08177が発生することがあります。分離レベルや読み取り一貫性の全体像は Oracleトランザクションの記事 も参考になります。

READ COMMITTED

Oracleの標準的な分離レベルです。各SQLの開始時点でコミット済みデータを読みます。多くの業務処理ではこちらで十分です。

SERIALIZABLE

トランザクション開始時点の一貫した見え方を保とうとします。競合があるとORA-08177で失敗することがあります。

ロック待ち

相手のコミットやロールバックを待つ状態です。ORA-08177は待つというより、整合性上続行できないと判断される点が違います。

デッドロック

互いに相手のロックを待つ状態です。詳しくは ORA-00060の記事 を参照してください。

よくある発生パターン

ORA-08177は、単発のSQLというより、トランザクション設計と同時実行の組み合わせで起きます。特にWebアプリ、バッチ、Spring、JPA、帳票生成、在庫や残高の更新処理で見かけます。

長いトランザクション

画面入力、複数SQL、外部API呼び出し、帳票作成までを同一トランザクションに入れていると競合しやすくなります。

同じ行を複数セッションが更新する

在庫、残高、採番、ステータス更新など、集中して更新される行があると発生しやすくなります。

バッチとオンライン処理が同時に動く

夜間バッチ、再計算処理、集計更新がオンライン更新とぶつかると、SERIALIZABLEでは失敗することがあります。

フレームワークで分離レベルを上げている

Springの @Transactional(isolation = Isolation.SERIALIZABLE) などで、意図せずSERIALIZABLEにしているケースがあります。

リトライ設計がない

ORA-08177は再実行で成功することがあります。リトライできない設計だと、利用者にそのままエラーが返ります。

再現例

次のように、2つのセッションで同じデータを扱うと、SERIALIZABLE分離レベルではORA-08177が発生することがあります。実際の再現可否はSQLの内容、索引、更新タイミング、Oracleのバージョンによって変わりますが、考え方としては「開始後に別トランザクションが更新したデータを、自分のトランザクションで安全に扱えない」状態です。

session1.sql
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

SELECT quantity
FROM stock
WHERE item_id = 100;

-- このトランザクションはまだCOMMITしない
session2.sql
UPDATE stock
SET quantity = quantity - 1
WHERE item_id = 100;

COMMIT;
session1-continue.sql
UPDATE stock
SET quantity = quantity - 1
WHERE item_id = 100;

-- ORA-08177: can't serialize access for this transaction

このような処理では、セッション1をそのまま続行するのではなく、トランザクション全体をロールバックして、必要に応じて最初から再実行する設計が必要です。

まず確認すること

ORA-08177が出た時は、SQL単体だけを見ると原因が分かりにくいです。トランザクション全体、分離レベル、同時実行の状況を確認します。

  • アプリやジョブでSERIALIZABLEを指定していないか
  • エラーが出た処理の開始から終了までが長すぎないか
  • 同じ行や同じ業務キーを複数処理が同時に更新していないか
  • バッチとオンライン処理が同じテーブルを同時更新していないか
  • 再実行すれば成功する一時的な競合か
  • リトライ時に二重登録や二重送信が起きない設計か
check-current-isolation-level.sql
SELECT SYS_CONTEXT('USERENV', 'ISOLATION_LEVEL') AS isolation_level
FROM dual;

上のSQLは、自分のセッションの現在の分離レベルを確認するためのものです。他セッションの分離レベルをこのSQLで直接見ることはできないため、アプリ設定、接続初期化SQL、実行ログと合わせて確認します。発生中のトランザクションやセッションを調べる場合は、次のように V$SESSIONV$TRANSACTION を見ます。

check-active-transactions.sql
SELECT s.sid,
       s.serial#,
       s.username,
       s.status,
       s.module,
       s.program,
       t.used_ublk,
       t.start_time
FROM v$session s
LEFT JOIN v$transaction t ON t.ses_addr = s.saddr
WHERE s.username IS NOT NULL
ORDER BY t.start_time NULLS LAST;

V$SESSIONV$TRANSACTION が見られない場合は、DBAに発生時刻、ユーザー、画面名、バッチ名、SQL_ID、処理開始時刻を渡します。セッションやロック調査の基本は セッション確認の記事 も参考になります。

対策1: トランザクションを短くする

ORA-08177の根本対策として、まずトランザクションを短くします。トランザクションが長いほど、その間に他セッションがデータを更新する可能性が上がります。

避けたい形

画面表示、ユーザー入力待ち、外部API呼び出し、ファイル出力を同じDBトランザクション内で行う。

改善した形

DB更新に必要な最小範囲だけをトランザクションにし、外部I/Oや重い集計は外へ出す。

バッチ処理

大量行を1トランザクションで処理せず、業務的に安全な単位で分割してコミットする。

画面処理

読み取り時と更新時の間が長い場合は、更新時にバージョン番号や更新日時で楽観ロックを確認する。

対策2: リトライできる設計にする

ORA-08177は、同時実行の一時的な競合で発生することがあります。そのため、処理内容が安全に再実行できるなら、トランザクション全体をロールバックして短い間隔でリトライする設計が有効です。

retry-policy-pseudocode.txt
max_retry = 3

for attempt in 1..max_retry:
    begin transaction
    try:
        execute business update
        commit
        break
    catch ORA-08177:
        rollback
        wait short random interval
        retry from the beginning
途中から再開しない
ORA-08177が出たら、失敗したSQLだけをもう一度実行するのではなく、基本的にはトランザクション全体を最初からやり直します。途中までの前提が古くなっている可能性があるためです。外部送信、メール送信、採番、決済などが絡む場合は、冪等性と二重実行対策を先に設計します。

PL/SQLで例外処理を組む場合は、PL/SQL例外処理の記事ORA-06512の読み方 も参考になります。

対策3: 分離レベルを見直す

本当にSERIALIZABLEが必要かを確認します。「なんとなく安全そうだから」という理由でSERIALIZABLEにしている場合、OracleではORA-08177の発生リスクを高めるだけになることがあります。

set-transaction-read-committed.sql
-- Oracleの一般的な分離レベル
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 必要な処理だけ実行してCOMMITする
UPDATE stock
SET quantity = quantity - 1
WHERE item_id = :item_id;

COMMIT;

厳密な一貫性が必要な処理では、SERIALIZABLEに頼るだけでなく、業務キー単位の排他、キュー化、楽観ロック、処理順序の制御を検討します。分離レベル別の挙動を検証したい場合は トランザクション分離レベルの検証記事 も参考になります。

対策4: 競合しやすい処理を直列化する

同じ業務キーに更新が集中する場合は、すべてをSERIALIZABLEで守ろうとするより、競合箇所だけを明示的に直列化する方が分かりやすいことがあります。たとえば在庫、残高、採番、締め処理などです。

select-for-update.sql
SELECT quantity
FROM stock
WHERE item_id = :item_id
FOR UPDATE;

UPDATE stock
SET quantity = quantity - :qty
WHERE item_id = :item_id;

SELECT FOR UPDATE は競合を分かりやすくしますが、ロック待ちやタイムアウトの設計が必要です。ロック待ちの考え方は ORA-00054の記事、複数処理の排他設計は マルチセッション環境の排他制御記事 も参考になります。

対策5: 楽観ロックで更新競合を検出する

画面で読み込んだデータを後から更新する処理では、SERIALIZABLEに頼るより、バージョン番号や更新日時を使った楽観ロックの方が扱いやすいことがあります。更新時に読み込み時点のバージョンと一致するか確認し、一致しなければ利用者へ再読み込みを促します。

optimistic-lock-update.sql
UPDATE orders
SET status = :new_status,
    version_no = version_no + 1,
    updated_at = SYSTIMESTAMP
WHERE order_id = :order_id
  AND version_no = :loaded_version_no;

-- 更新件数が0件なら、他の処理が先に更新している

楽観ロックでは、競合をエラーとしてDB任せにするのではなく、業務的に分かる形で「他の人が更新済みです」と扱えます。在庫や残高のように厳密な排他が必要な箇所は明示的なロック、画面更新の競合検出は楽観ロック、というように使い分けると設計が読みやすくなります。

SpringやJavaで確認するポイント

JavaやSpringでORA-08177が出る場合、DB側だけでなくアプリ側のトランザクション設定を確認します。特に共通設定やサービス層のアノテーションで、意図せずSERIALIZABLEが指定されていることがあります。

spring-serializable-example.java
@Transactional(isolation = Isolation.SERIALIZABLE)
public void updateStock(Long itemId, int qty) {
    // 競合が多い処理ではORA-08177が出やすくなることがある
}
spring-retry-image.java
@Retryable(
    include = CannotSerializeTransactionException.class,
    maxAttempts = 3
)
@Transactional
public void updateWithRetry(Long id) {
    // トランザクション全体を再実行できる形にする
}

実装では、例外クラス名やリトライ対象は利用しているドライバ、Springのバージョン、例外変換設定によって変わります。ログにはOracleエラーコード 8177、SQLState、例外クラス、リトライ回数を残しておくと、あとから原因を追いやすくなります。重要なのは、ORA-08177だけを握りつぶさず、再実行可能な処理に限定してリトライすることです。

似たエラーとの違い

ORA-08177は、ロックや読み取り一貫性のエラーと混同されやすいです。発生条件と対策が違うため、エラー番号ごとに切り分けます。

ORA-08177

SERIALIZABLE分離レベルで直列化できない競合が起きています。リトライ、短いトランザクション、分離レベル見直しを検討します。

ORA-00060

デッドロックです。互いにロックを待っているため、SQLの順序や更新順を見直します。詳しくは ORA-00060の記事 を参照してください。

ORA-00054

NOWAITやDDLでロック取得に失敗しています。詳しくは ORA-00054の記事 を参照してください。

ORA-01555

読み取り一貫性を保つためのUNDOが不足、または古くなった状態です。詳しくは ORA-01555の記事 を参照してください。

ロック待ち

エラーではなく待機状態として表れることがあります。V$SESSION の待機イベントやブロッキングセッションを確認します。

対応手順まとめ

  1. アプリやSQLで SERIALIZABLE を指定していないか確認する
  2. 発生した処理のトランザクション範囲を確認する
  3. 同じ業務キーを複数処理が同時更新していないか確認する
  4. 再実行すれば成功する一時的な競合かを確認する
  5. 安全にリトライできるよう、処理を冪等にする
  6. 長いトランザクションを短くする
  7. 本当に必要な箇所以外は READ COMMITTED を検討する
  8. 競合が集中する業務キーは、キュー化や SELECT FOR UPDATE で明示的に制御する

ORA-08177は、Oracleが壊れているというより、SERIALIZABLE分離レベルで同時実行を許した結果、トランザクションを安全に続行できなくなったというサインです。単純にエラーを無視したり、失敗したSQLだけを再実行したりすると、業務データの整合性を崩す可能性があります。

まず分離レベルとトランザクション範囲を確認し、競合が起きやすい処理を短く、再実行可能に、必要なら直列化する方向で設計を見直してください。

よくある質問

ORA-08177はリトライしてよいですか?

処理が冪等で、トランザクション全体を最初からやり直せるならリトライ候補です。外部送信や採番が絡む場合は、二重実行対策を先に確認します。

分離レベルをREAD COMMITTEDに下げれば解決しますか?

解決することはありますが、業務上SERIALIZABLEが必要な理由があるなら慎重に判断します。多くの更新処理では、READ COMMITTEDと楽観ロック、または明示的な排他制御の組み合わせで十分なことがあります。

ORA-08177とデッドロックは同じですか?

違います。デッドロックは互いにロックを待つ状態ですが、ORA-08177はSERIALIZABLEトランザクションを整合した形で続行できない状態です。

本番でまず何を見ればよいですか?

発生時刻、処理名、分離レベル、トランザクション開始からエラーまでの時間、同じキーを更新する別処理の有無を確認します。その上で、DBAにセッション情報やSQL_IDの確認を依頼します。

参考