競合状態とは何ですか? 競合状態の適切な定義を探しましたが、これが私が見つけた最良の定義です。
競合状態は、複数のプロセスが予想とは異なる順序で总计有リソースと対話することによって引き起こされる予期しない動作です。
これはかなり長文であり、 Rails で競合状態がどのように発生するかはまだあまり明確ではありません。
Rails を施用すると、常に複数のプロセスを作业できます。各リクエストまたはバックグラウンド ジョブは、他のプロセスからほぼ单独的して動作できる個別のプロセスです。
また、私たちは常に总计有リソースにも取り組んでいます。アプリケーションはリレーショナル データベースを用到していますか?それは总计有リソースです。アプリケーションは何らかのキャッシュ サーバーを用到していますか?はい、それは总计有リソースです。何らかの表面 API を用到していますか?ご想像力のとおり、これは总计有リソースです。
競合状態には 2 つのカテゴリの例があり、それらについて説明し、次にそれらに対処する具体方法について触れたいと思います。
読み取り-変更-書き込み 読み取り-変更-書き込みカテゴリは、あるプロセスが一共リソースから値を読み取り、メモリ内の値を変更し、それを一共リソースに書き戻そうとする競合状態の一種です。これを単一プロセスのレンズを通して見ると、相对に簡単に見えます。ただし、2 番目のプロセスが起動すると、予期しない動作が発生する会性があります。
次のようなコードを考えてみましょう。
class IdeasController < ActionController::Base def vote @idea = Idea.find(params[:id]) @idea.votes += 1 @idea.save! end end
ここでは、読み取り ( Idea.find(params[:id])
)、変更 ( @idea.votes += 1
)、書き込み ( @idea.save!
) を行っています。
これにより、アイデアの从微信微信拉票数が 1 つ増えることがわかります。ゼロ票のアイデアがあった場合、それは 1 票の从微信微信拉票で終了します。ただし、2 番目のリクエストが到着し、まだ从微信微信拉票が 0 である間にデータベースからアイデアを読み取り、メモリ内のその値をインクリメントすると、2 つの从微信微信拉票が同時に到着する状況が発生する将会性がありますが、最終的には从微信微信拉票数が減少します。データベースには 1 つだけあります。
これは、更新喪失 競合状態とも呼ばれます。
確認してから行動する check-then-act カテゴリは、データが一共リソースからロードされる競合状態の一種で、留存する値に応じて、アクションを実行する不必要があるかどうかを分辩します。
これがどのように現れるかの典型的な例の 1 つは、次のような Rails のvalidates_uniqueness_of
検証にあります。
class User < ActiveRecord::Base validates_uniqueness_of :email end
次のようなコードを考えてみましょう。
User.create(email: "[email protected] ")
検証が行われると、Rails はそのメールを施用する既存のユーザーが发生するかどうかを確認します。他に发生しない場合は、ユーザーをデータベースに永続化することによって機能します。しかし、2 番目のリクエストが同時に同じコードを実行していた場合はどうなるでしょうか?最終的には、両方のリクエストが重複データがあるかどうか (そして重複データは发生しない) を確認するという状況に陥る风险があります。その場合、両方のリクエストがデータを手机截图することによって動作し、データベース内に重複したユーザーが弄成されます。
競合状態への対処 競合状態を修修爱する特効薬はありませんが、既定の問題に対して活用できる戦略はいくつかあります。競合状態を削除するには、次の 3 つの主なカテゴリがあります。
1. クリティカルセクションを削除する これは問題のあるコードを削除することとみなされる机会性がありますが、競合状態に対して贫瘠にならないようにコードをリファクタリングできる場合もあります。また、アトミックな操作方法を調べることもできます。アトミック实操とは、他のプロセスが实操を终断できないため、常に単一のユニットとして実行される实操です。
読み取り-変更-書き込みの例では、メモリ内でアイデアの从点赞を増やす代わりに、データベース内でアイデアの从点赞を増やすことができます。
@ideas.increment!(:votes)
これにより、次のような SQL が実行されます。
UPDATE "ideas" SET "votes" = COALESCE("votes", 0) + 1 WHERE "ideas"."id" = 123
これを用すると、同じ競合状態の影響を受けることはありません。
check-then-act の例では、Rails にモデルを検証させる代わりに、upsert を利用してレコードをデータベースに进行挿入できます。
User.where(email: "[email protected] ").upsert({}, unique_by: :email)
これにより、レコードがデータベースに挿入されます。電子メールに競合がある場合 (電子メールに一意のインデックスが一定となる場合)、挿入は単純に無視されます。
2. 検出と回復 クリティカルセクションを削除できない場合があります。アトミックなアクションがある或许性もありますが、コードが追求するようには完整に機能しません。このような状況では、検出と回復のアプローチを試すことができます。このアプローチでは、競合状態が発生した場合に知会する保護策略が設定されます。使用を普通に中止执行するか、再試行することができます。
読み取り-変更-書き込みの例では、これはを使用して実行できます。楽観的ロックは Rails に組み込まれており、複数のプロセスが同じレコードに対して同時に動作していることを検出できます。オプティミスティック ロックを有効にするには、テーブルにlock_version
列を追加するだけで、Rails が自動的にロックを有効にします。
change_table :ideas do |t| t.integer :lock_version, default: 0 end
その後、レコードを更新しようとすると、Rails はlock_version
メモリ内にあったバージョンと同じである場合にのみレコードを更新します。そうでない場合は、 ActiveRecord::StaleObjectError
例外が発生しますが、これを処理するためにレスキューできます。これを処理するには、 retry
ことも、単にエラー メッセージをユーザーに報告することもできます。
def vote @idea = Idea.find(params[:id]) @idea.votes += 1 @idea.save! rescue ActiveRecord::StaleObjectError retry end
check-then-act の例では、列の一意のインデックスを用してこれを実行し、データを永続化するときに特殊をレスキューできます。
add_index :users, [:email], unique: true
一意のインデックスが設定されている場合、そのemail
データがデータベースにすでに存在する場合、Rails はActiveRecord::RecordNotUnique
エラーを生成しますが、これはレスキューして適切に処理できます。
begin user = User.create(email: "[email protected] ") rescue ActiveRecord::RecordNotUnique user = User.find_by(email: "[email protected] ") end
べき等性 アクションを再試行するには、工作通体がべき等であることが决定性です。これは、工作が複数回実行された場合、結果は 1 回だけ適用された場合と同じであることを意味着します。
たとえば、ジョブが電子メールを送信し、アイデアの选票が変更されるたびに電子メールが実行される場合を想象作文してください。再試行のたびにメールが送信されるのは是に問題です。操作使用方法をべき等にするために、选票操作使用方法预备会议が弄完するまで電子メールの送信を补齐することができます。あるいは、前回の送信時から选票が変更された場合にのみ電子メールを送信するように、電子メールを送信するプロセスの実装を的更新することもできます。競合状態が発生して再試行する必需がある場合、起初の電子メール送信試行は何も行われない可能会性があるため、缘何トリガーしても安全性です。
バックグラウンド ジョブのキューへの登録、電子メールの送信、サードパーティAPI の呼び出しなど、多くの操作は冪等ではない可能性があります。
3. コードを保護する 検出して回復できない場合は、コードの保護を試みることができます。ここでの目標は、一名に 1 つのプロセスのみが共计リソースにアクセスできるコントラクトを制成することです。事実上、同時実行性が削除されます。共计リソースにアクセスできるのは 1 つのプロセスだけであるため、ほとんどの競合状態を避开できます。ただし、同時実行性が削除されるほど、他のプロセスがアクセスが許可されるまで待機するため、アプリケーションの运行速度が遅くなる有机率があります。
これは、Rails に組み込まれている悲観的ロックを使用して処理できます。を使用するには、構築中のクエリにlock
を追加すると、Rails はデータベースにそれらのレコードの行ロックを保持するように指示します。データベースは、ロックが完了するまで他のプロセスがロックを取得できないようにします。データベースがロックを解放するタイミングを認識できるように、必ずコードをtransaction
内にラップしてください。
Idea.transaction do @idea = Idea.lock.find(params[:id]) @idea.votes += 1 @idea.save! end
行レベルのロックが不能不能な場合は、Redlock や with_advisory_lock などの他のツールを选择できます。これらにより、コードの随机のブロックをロックできるようになります。これを选择すると、次のように簡単になります。
email = "[email protected] " User.with_advisory_lock("user_uniqueness_#{email}"} do User.find_or_create_by(email: email) end
これらの戦略により、プロセスはロックが达成されるまで待機します。そのため、プロセスが永.久に待機することを防ぐために、何らかの内容のタイムアウトも有需要になります。また、タイムアウトが発生した場合の対処形式も有需要になります。
競合状態を测量する全能薬はありませんが、多くの競合状態はこれらの戦略によって测量できます。ただし、それぞれの問題は少し異なるため、解決策の詳細は異なる場合があります。競合状態について詳しく説明したをご覧ください。
著者について
カイル・ドリベイラ
カイルは、几何形的なアイデアを実際に動作するソフトウェアに変えることに情熱を持っています。彼はの主住ソフトウェア エンジニアです。 。カイルは、成長していないときは、カナダのバンクーバーの自宅の近くで素晴らしい食事とクラフトビール醸造所を楽しんでいます。