トランザクション自体では、多くの一般的な同時実行シナリオから保護されず、カウンターをインクリメントすることはその 1 つであることは間違いありません。ロックを強制する一般的な方法はありません。コード内の必要な場所で確実に使用する必要があります
単純なカウンターのインクリメントのシナリオでは、うまく機能するメカニズムが 2 つあります。
行ロック
行のロックは、コード内の重要な場所で行う限り機能します。どこが重要なのかを知るには、:/. 上記のコードのように、リソースが同時実行保護を必要とする場所が 2 つあり、1 つだけをロックすると、同時実行の問題が発生します。
with_lock
フォームを使用したい。これはトランザクションと行レベルのロックを行います (テーブル ロックは明らかに行ロックよりもはるかに貧弱にスケーリングされますが、行数が少ないテーブルの場合、postgresql (mysql については不明) がとにかくテーブル ロックを使用するため、違いはありません。これは次のようになります。
v = Vote.first
v.with_lock do
v.vote +=1
sleep 10
v.save
end
はwith_lock
、トランザクションを作成し、オブジェクトが表す行をロックし、オブジェクトの属性をすべて 1 つのステップで再ロードするため、コードにバグが発生する可能性が最小限に抑えられます。ただし、これは、複数のオブジェクトの相互作用に関連する同時実行の問題に必ずしも役立つとは限りません。a) 可能なすべての相互作用が 1 つのオブジェクトに依存し、常にそのオブジェクトをロックし、b) 他のオブジェクトがそれぞれそのオブジェクトの 1 つのインスタンスとのみ相互作用する場合、たとえばユーザー行をロックし、すべてが属するオブジェクトと何かを行う場合に機能します (おそらく間接的に)そのユーザー オブジェクト。
シリアライズ可能なトランザクション
もう 1 つの可能性は、シリアライズ可能なトランザクションを使用することです。9.1 以降、Postgresql には「実際の」シリアライズ可能なトランザクションがあります。これは、行をロックするよりもはるかに優れたパフォーマンスを発揮します (ただし、単純なカウンターのインクリメントのユースケースでは問題にならない可能性があります)。
シリアライズ可能なトランザクションがもたらすものを理解する最良の方法は次のとおりです。アプリ内のすべてのトランザクションのすべての可能な順序を取得すると(isolation: :serializable)
、アプリの実行時に起こることは常にそれらの順序の 1 つに対応することが保証されます。通常のトランザクションでは、これが正しいとは限りません。
ただし、代わりに行う必要があるのは、トランザクションがシリアライズ可能であることをデータベースが保証できないためにトランザクションが失敗したときに何が起こるかを処理することです。カウンターのインクリメントの場合、必要なことはretry
次のとおりです。
begin
Vote.transaction(isolation: :serializable) do
v = Vote.first
v.vote += 1
sleep 10 # this is to simulate concurrency
v.save
end
rescue ActiveRecord::StatementInvalid => e
sleep rand/100 # this is NECESSARY in scalable real-world code,
# although the amount of sleep is something you can tune.
retry
end
再試行前のランダム スリープに注意してください。これが必要なのは、失敗したシリアル化可能なトランザクションのコストが重要であるためです。スリープしないと、複数のプロセスが同じリソースをめぐって競合し、データベースが圧倒される可能性があります。同時実行が多いアプリでは、再試行のたびにスリープを徐々に増やす必要がある場合があります。乱数は高調波デッドロックを回避するために非常に重要です。すべてのプロセスが同じ時間だけスリープすると、互いにリズムを取り、すべてがスリープし、システムがアイドル状態になり、すべてのプロセスが次の時間にロックしようとします。同時にシステムがデッドロックし、1 つを除くすべてが再びスリープ状態になります。
シリアライズが必要なトランザクションに、データベース以外の並行性のソースとのやり取りが含まれる場合でも、必要なことを達成するために行レベルのロックを使用する必要がある場合があります。この例としては、ステート マシンの遷移が、サードパーティ API などのデータベース以外へのクエリに基づいて、どの状態に遷移するかを決定する場合があります。この場合、サードパーティ API がクエリされている間、オブジェクトを表す行をステート マシンでロックする必要があります。シリアライズ可能なトランザクション内にトランザクションをネストすることはできないため、object.lock!
代わりにを使用する必要がありwith_lock
ます。
注意すべきもう 1 つのことは、外部でフェッチされたオブジェクトは、トランザクション内で使用する前に呼び出すtransaction(isolation: :serializable)
必要があるということです。reload