web-dev-qa-db-ja.com

行ロック競合の追跡、デバッグ、修正

遅く、私は多くの行ロック競合に直面しています。競合しているテーブルは特定のテーブルのようです。

これは一般的に何が起こるかです-

  • 開発者1は、Oracle Formsフロントエンド画面からトランザクションを開始します。
  • 開発者2は、同じ画面を使用して別のセッションから別のトランザクションを開始します

5分以内にフロントエンドが応答しないようです。セッションをチェックすると、行ロックの競合が表示されます。誰もが投げかける「解決策」は、セッションを強制終了することです:/

データベース開発者として

  • 行ロックの競合を解消するにはどうすればよいですか?
  • ストアドプロシージャのどの行がこれらの行ロック競合を引き起こしているのかを見つけることは可能ですか?
  • どのようなコーディングをこのような問題を軽減/回避/排除するための一般的なガイドラインは何でしょうか?

この質問が自由回答/不十分な情報であると思われる場合は、自由に編集/知らせてください-いくつかの追加情報を追加できるように最善を尽くします。


問題のテーブルは多くの挿入と更新の下にあります、私はそれが最も忙しいテーブルの1つだと思います。 SPはかなり複雑です-簡単にするために-さまざまなテーブルからデータをフェッチし、それをワークテーブルに入力します。多くの算術演算がワークテーブルで発生し、ワークテーブルの結果が挿入されます/問題の表に更新されました。


データベースのバージョンは、Oracle Database 10g Enterprise Editionリリース10.2.0.1.0-64ビットです。ロジックのフローは両方のセッションで同じ順序で実行され、トランザクションは長時間開いたままになりません(または少なくともIthinkso)。 、およびロックはトランザクションのアクティブな実行中に発生します。


pdate:テーブルの行数が予想よりも多く、約310万行です。また、セッションをトレースした後、このテーブルに対するいくつかの更新ステートメントがインデックスを利用していないことがわかりました。なぜそうなのか、よくわかりません。 where句で参照される列にはインデックスが付けられます。現在、インデックスを再構築しています。

12
Sathyajith Bhat

ストアドプロシージャのどの行がこれらの行ロック競合を引き起こしているのかを見つけることは可能でしょうか?

正確ではありませんが、ロックを引き起こしているSQLステートメントを取得して、プロシージャ内の関連する行を特定できます。

_SELECT sid, sql_text
FROM v$session s
LEFT JOIN v$sql q ON q.sql_id=s.sql_id
WHERE state = 'WAITING' AND wait_class != 'Idle'
AND event = 'enq: TX - row lock contention';
_

コーディングに関するこのような問題を軽減/回避/排除するための一般的なガイドラインは何でしょうか?

ロックに関するOracleコンセプトガイドのセクション は、「行はライターによって変更された場合にのみロックされる」と述べています。同じ行を更新する別のセッションは、最初のセッションがCOMMITまたはROLLBACKになるまで待機してから続行できます。問題を解消するためにユーザーをシリアル化することができますが、問題を問題ではないレベルまで減らすことができるいくつかの方法を次に示します。

  • COMMITより頻繁に。すべてのCOMMITはロックを解放するため、更新をバッチで実行できる場合は、別のセッションが同じ行を必要とする可能性が低くなります。
  • 値を変更せずに行を更新していないことを確認してください。たとえば、UPDATE t1 SET f1=DECODE(f2,’a’,f1+1,f1);は、より選択的な(より少ないロックを読み取る)_UPDATE t1 SET f1=f1+1 WHERE f2=’a’;_として書き換える必要があります。もちろん、ステートメントを変更してもテーブル内の大部分の行がロックされる場合は、変更によって読みやすさが向上します。
  • テーブルをロックして現在の最高値に追加するのではなく、シーケンスを使用していることを確認してください。
  • インデックスが使用されない原因となっている関数を使用していないことを確認してください。関数が必要な場合は、関数ベースのインデックスにすることを検討してください。
  • セットで考えます。更新を実行するPL/SQLのブロックを実行するループを単一の更新ステートメントとして書き直すことができるかどうかを検討してください。そうでない場合は、おそらく _BULK COLLECT ... FORALL_ で一括処理を使用できます。
  • 最初のUPDATECOMMITの間で行われる作業を減らします。たとえば、更新のたびにコードがメールを送信する場合は、メールをキューに入れ、更新をコミットした後に送信することを検討してください。
  • _SELECT ... FOR UPDATE NOWAIT_または_WAIT 2_を実行して、待機を処理するようにアプリケーションを設計します。次に、行をロックできないことをキャッチし、別のセッションが同じデータを変更していることをユーザーに通知できます。
10
Leigh Riffel

開発者の立場から回答をさせていただきます。

私の意見では、説明したような行の競合が発生した場合、それはアプリケーションにバグがあるためです。ほとんどの場合、このタイプの競合は、更新が失われる脆弱性の兆候です。これは AskTomのスレッド が失われた更新の概念を説明しています。

更新が失われるのは、次の場合です。

セッション1:トムの従業員レコードを読み取る

セッション2:トムの従業員レコードを読み取る

セッション1:トムの従業員レコードを更新する

セッション2:トムの従業員レコードを更新する

セッション2は、セッション1の変更を確認せずに上書きし、更新が失われます。

更新が失われたことによる厄介な副作用が発生しました。セッション1がまだコミットされていないため、セッション2がブロックされる可能性があります。ただし、主な問題は、セッション2が盲目的にレコードを更新することです。両方のセッションがステートメントを発行するとします。

UPDATE table SET col1=:col1, ..., coln=:coln WHERE id = :pk

両方のステートメントの後、行がセッション1によって変更されたことがセッション2に通知されずに、セッション1の変更が上書きされました。


失われた更新(および競合の副作用)は決して発生してはなりません。100%回避できます。ロックを使用するには、2つの主要な方法 楽観的および悲観的ロック でそれらを防止する必要があります。

1)悲観的ロック

行を更新したい。このモードでは、その行のロックを要求することにより、他のユーザーがこの行を変更できないようにします(SELECT ... FOR UPDATE NOWAITステートメント)。行がすでに変更されている場合は、エラーメッセージが表示されます。このメッセージはエンドユーザーに適切に変換できます(この行は別のユーザーによって変更されています)。行が使用可能な場合は、変更(UPDATE)を行い、トランザクションが完了するたびにコミットします。

2)楽観的ロック

行を更新したい。ただし、その行のロックを維持したくない場合は、おそらく複数のトランザクションを使用して行を更新するため(Webベースのステートレスアプリケーション)、またはユーザーが長時間ロックを保持することを望まない場合があります(他の人がブロックされる可能性があります)。その場合、すぐにロックをリクエストすることはありません。マーカーを使用して、更新が発行されるときに行が変更されていないことを確認します。すべての列の値をキャッシュするか、自動的に更新されるタイムスタンプ列またはシーケンスベースの列を使用できます。選択内容が何であれ、更新を実行しようとしているときに、次のようなクエリを発行して、その行のマーカーが変更されていないことを確認します。

SELECT <...>
  FROM table
 WHERE id = :id
   AND marker = :marker
   FOR UPDATE NOWAIT

クエリが行を返す場合は、更新を行います。そうでない場合は、最後にクエリを実行してから行が変更されていることを意味します。プロセスを最初からやり直す必要があります。

注:DBにアクセスするすべてのアプリケーションを完全に信頼している場合は、楽観的ロックの直接更新を利用できます。あなたは直接発行することができます:

UPDATE table
   SET <...>, 
       marker = marker + 1
 WHERE id = :id;

ステートメントが行を更新しない場合、誰かがこの行を変更したため、最初からやり直す必要があります。

すべてのアプリケーションがこのスキームに同意する場合、他の誰かによってブロックされることはなく、ブラインドアップデートを回避します。ただし、行を事前にロックしない場合でも、別のアプリケーション、バッチジョブ、または直接更新が楽観的ロックを実装していない場合は、無期限にロックされる可能性があります。これが、ロックスキームの選択に関係なく、常に行をロックすることをお勧めする理由です(行をロックすると、rowidを含むすべての値を取得するため、パフォーマンスへの影響は無視できます)。

TL; DR

  • 事前にロックせずに行を更新すると、アプリケーションが「フリーズ」する可能性があります。これは、DBへのすべてのDMLが楽観的または悲観的ロックを実装する場合に回避できます。
  • SELECTステートメントが以前のSELECTと一致する値を返すことを確認します(失われた更新の問題を回避するため)。
7
Vincent Malgrat

この答えはおそらく、The Daily WTFのエントリーに適格です。

右、セッションを追跡してUSER_SOURCEを検索した後-根本的な原因を追跡しました

  • その原因は、当然のことながら、ロジックの欠陥でした
  • 最近、更新ステートメントがSPに追加されました。 updateステートメントは基本的にテーブル全体を更新します。どうやら問題の開発者は、必要なステートメントを更新するための正しいwhere句を追加することを忘れていました。
  • 更新されるテーブルは前述のとおり、最もトランザクションの多いテーブルの1つであり、多数のレコードがありました。更新には長い時間を要します。
  • その結果、他のセッションはテーブルをロックできず、行ロックの競合状態に陥っていました。
5
Sathyajith Bhat