3

Rails では複数のサーバーで同時実行の問題が発生する可能性があることに気付き、モデルを常にロックさせたいと考えています。データの整合性を強制する一意の制約と同様に、これはRailsで可能ですか? それとも、注意深いプログラミングが必要なだけですか?

ターミナル 1

irb(main):033:0* Vote.transaction do
irb(main):034:1* v = Vote.lock.first
irb(main):035:1> v.vote += 1
irb(main):036:1> sleep 60
irb(main):037:1> v.save
irb(main):038:1> end

第二ターミナル、寝てる間に

irb(main):240:0* Vote.transaction do
irb(main):241:1* v = Vote.first
irb(main):242:1> v.vote += 1
irb(main):243:1> v.save
irb(main):244:1> end

DB 開始

 select * from votes where id = 1;
 id | vote |         created_at         |         updated_at         
----+------+----------------------------+----------------------------
  1 |    0 | 2013-09-30 02:29:28.740377 | 2013-12-28 20:42:58.875973 

実行後

ターミナル 1

irb(main):040:0> v.vote
=> 1

ターミナル 2

irb(main):245:0> v.vote
=> 1

DBエンド

select * from votes where id = 1;
 id | vote |         created_at         |         updated_at         
----+------+----------------------------+----------------------------
  1 |    1 | 2013-09-30 02:29:28.740377 | 2013-12-28 20:44:10.276601 

その他の例

http://rhnh.net/2010/06/30/acts-as-list-will-break-in-production

4

4 に答える 4

9

トランザクション自体では、多くの一般的な同時実行シナリオから保護されず、カウンターをインクリメントすることはその 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

于 2014-05-16T06:22:41.853 に答える
0

ActiveRecord は常に保存操作をトランザクションでラップします。

単純なケースでは、Ruby でロジックを実行してから保存するのではなく、SQL 更新を使用するのが最善かもしれません。これを行うためのモデル メソッドを追加する例を次に示します。

class Vote
  def vote!
    self.class.update_all("vote = vote + 1", {:id => id})
  end

この方法により、例でロックする必要がなくなります。より一般的なデータベース ロック チェックが必要な場合は、David の提案を参照してください。

于 2013-12-29T00:13:36.803 に答える
0

モデルで次のように実行できます

class Vote < ActiveRecord::Base

validate :handle_conflict, only: :update
attr_accessible :original_updated_at
attr_writer :original_updated_at

def original_updated_at
  @original_updated_at || updated_at 
end 

def handle_conflict
    #If we want to use this across multiple models
    #then extract this to module
    if @conflict || updated_at.to_f> original_updated_at.to_f
      @conflict = true
      @original_updated_at = nil
      #If two updates are made at the same time a validation error
      #is displayed and the fields with
      errors.add :base, 'This record changed while you were editing'
      changes.each do |attribute, values|
        errors.add attribute, "was #{values.first}"
      end
    end
  end
end 

設定されるoriginal_updated_at仮想属性です。handle_conflictレコードが更新されたときに発生します。updated_at属性がデータベースにあるかどうかを確認します (ページで定義されている) 非表示の属性よりも後です。ところで、次のように定義する必要がありますapp/view/votes/_form.html.erb

<%= f.hidden_field :original_updated_at %>

競合がある場合は、検証エラーを発生させます。

また、Rails 4 を使用している場合は、attr_accessible がなく、コントローラーのメソッドに追加する必要があり:original_updated_atますvote_params

うまくいけば、これはいくつかの光を当てます。

于 2013-12-29T02:29:03.670 に答える