行ロック競合のトレース、デバッグ、修正


12

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

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

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

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

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

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

この質問が無制限で不十分な情報であると感じた場合は、気軽に編集してください。


問題のテーブルには多くの挿入と更新が行われています。最も忙しいテーブルの1つだと思います。SPはかなり複雑です-簡単にするために-さまざまなテーブルからデータをフェッチし、それを作業テーブルに取り込みます。作業テーブルで多くの算術演算が行われ、作業テーブルの結果が問題のテーブルに挿入/更新されます。


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


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


1
@Sathya-ストアドプロシージャの複雑さを詳しく説明できますか?疑わしいテーブルは厳密に更新または挿入されていますか?
CoderHawk

ここで外部キーが役割を果たしますか?(これにはインデックスが必要な場合があります)データベースのバージョンは何ですか?ロジックのフローは両方のセッションで同じ順序で実行されますか?トランザクションは長時間「オープン」のままですか?ユーザーが考える時間中に、またはトランザクションのアクティブな実行中にロックが発生しますか?
ik_zelf

@サンディ私は質問を更新しました
サティアジスバート

@ik_zelf質問を更新しました
サティアジスバート

1
なぜこれが問題なのかは明確ではありません。Oracleは、1行へのアクセスをシリアル化するという本来の動作を正確に行っています。誰かがその行を持っている場合、前のバージョンを読むことができますが、書き込むにはロックを解除するのを待つ必要があります。それに対する唯一の「修正」は、a)馬鹿になっCOMMITたりROLLBACK、妥当な時間内に行ったりしないこと、またはb)同じ人が常に同じ行を必要としないようにすることです。
ガイウス

回答:


10

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

正確ではありませんが、ロックの原因となっている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 Concepts Guide』セクションには、「ライターによって変更された場合にのみ行がロックされる」と書かれています。同じ行を更新する別のセッションは、最初のセッションが継続するまで、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
  • 最初UPDATEとの間で行われる作業を減らしCOMMITます。たとえば、各更新後にコードが電子メールを送信する場合、更新のコミット後に電子メールをキューに入れて送信することを検討してください。
  • SELECT ... FOR UPDATE NOWAITまたはを実行して、待機を処理するアプリケーションを設計しますWAIT 2。その後、行をロックできず、別のセッションが同じデータを変更していることをユーザーに通知できます。

7

開発者の観点から答えを提供します。

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

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

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

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

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

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

セッション2は、セッション1の変更を目にすることなく上書きするため、更新が失われます。

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

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

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


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

1)悲観的ロック

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

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と一致する値を返すことを確認します(更新の損失の問題を回避するため)

5

この回答は、おそらくThe Daily WTFのエントリーにふさわしいでしょう。

右、セッションをトレースして検索した後USER_SOURCE-私は根本原因を追跡しました

  • 原因は、当然のことながら、欠陥のあるロジックでした
  • 最近、更新ステートメントがSPに追加されました。updateステートメントは、基本的にテーブル全体を更新します。どうやら問題の開発者は、必要なステートメントを更新するために適切なwhere句を追加することを忘れていたようです。
  • 更新中のテーブルは上記のとおりで、最も処理されたテーブルの1つであり、多数のレコードがありました。更新には時間がかかり、苦労します。
  • その結果、他のセッションはテーブルのロックを取得できず、行ロックの競合が発生しました。
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.