3

現在、Android 向けに作成しているゲームのライブ テストを行っています。サービスは Rails 3.1 で書かれており、私は Postgresql を使用しています。私のより技術に精通したテスターの何人かは、リクエストをサーバーに記録し、高い並行性でそれらを再生することで、ゲームを操作することができました。コードにとらわれることなく、以下のシナリオを簡単に説明しようと思います。

  • ユーザーは複数のアイテムを購入でき、各アイテムはデータベースに独自のレコードを持っています。
  • 要求はコントローラー アクションに送られ、トランザクションに関する情報を記録する購入モデルが作成されます。
  • トレード モデルには、アイテムの購入を設定するメソッドがあります。基本的に、いくつかの論理的な手順を実行して、アイテムを購入できるかどうかを確認します。最も重要なのは、いつでもユーザーごとに 100 アイテムの制限があることです。すべての条件が満たされると、要求された数のアイテムを作成するために単純なループが使用されます。

つまり、彼らが行っているのは、プロキシ経由で 1 つの有効なリクエストの購入を記録することです。次に、高い並行性でそれを再生します。これにより、本質的に、毎回いくつかの余分なものがすり抜けることができます。つまり、100 個購入するように設定した場合、300 ~ 400 個まで取得できます。また、15 個購入する場合は、120 個程度まで取得できます。

上記の購入方法はトランザクションでラップされます。ただし、ラップされていても、リクエストがほぼ同時に実行されている特定の状況では停止しません。これにはDBロックが必要になるかもしれないと推測しています。知っておく必要があるもう 1 つのひねりは、任意の時点で rake タスクがユーザー テーブルに対して cron ジョブで実行され、プレイヤーのヘルスとエネルギー属性を更新することです。したがって、それもブロックできません。

どんな支援も本当に素晴らしいでしょう。これは私のちょっとした趣味のサイド プロジェクトであり、ゲームが公正で誰にとっても楽しいものであることを確認したいと考えています。

本当にありがとう!

コントローラーのアクション:

  def hire
    worker_asset_type_id = (params[:worker_asset_type_id])
    quantity = (params[:quantity])

    trade = Trade.new()

    trade_response = trade.buy_worker_asset(current_user, worker_asset_type_id, quantity)

    user = User.find(current_user.id, select: 'money')

    respond_to do |format|
      format.json {
        render json: {
            trade: trade,
            user: user,
            messages: {
                messages: [trade_response.to_s]
            }
        }
      }
    end
  end

取引モデル方法:

def buy_worker_asset(user, worker_asset_type_id, quantity)
    ActiveRecord::Base.transaction do

      if worker_asset_type_id.nil?
        raise ArgumentError.new("You did not specify the type of worker asset.")
      end

      if quantity.nil?
        raise ArgumentError.new("You did not specify the amount of worker assets you want to buy.")
      end

      if quantity <= 0
        raise ArgumentError.new("Please enter a quantity above 0.")
      end

      quantity = quantity.to_i
      worker_asset_type = WorkerAssetType.where(id: worker_asset_type_id).first

      if worker_asset_type.nil?
        raise ArgumentError.new("There is no worker asset of that type.")
      end

      trade_cost = worker_asset_type.min_cost * quantity

      if (user.money < trade_cost)
        raise ArgumentError.new("You don't have enough money to make that purchase.")
      end

      # Get the users first geo asset, this will eventually have to be dynamic
      potential_total = WorkerAsset.where(user_id: user.id).length + quantity

      # Catch all for most people
      if potential_total > 100
        raise ArgumentError.new("You cannot have more than 100 dealers at the current time.")
      end

      quantity.times do
        new_worker_asset = WorkerAsset.new()
        new_worker_asset.worker_asset_type_id = worker_asset_type_id
        new_worker_asset.geo_asset_id = user.geo_assets.first.id
        new_worker_asset.user_id = user.id
        new_worker_asset.clocked_in = DateTime.now

        new_worker_asset.save!
      end

      self.buyer_id = user.id
      self.money = trade_cost
      self.worker_asset_type_id = worker_asset_type_id
      self.trade_type_id = TradeType.where(name: "market").first.id
      self.quantity = quantity

      # save trade
      self.save!

      # is this safe?
      user.money = user.money - trade_cost

      user.save!
    end
  end
4

2 に答える 2

4

リクエストのリプレイが無効になるように、べき等のリクエストが必要なようです。可能な場合は、操作を繰り返しても影響がないように操作を実装してください。不可能な場合は、各リクエストに一意のリクエスト識別子を付け、リクエストが満たされたかどうかを記録します。UNLOGGED永続化する必要がないため、リクエスト ID 情報を PostgreSQL または redis/memcached のテーブルに保持できます。これにより、エクスプロイトのクラス全体が防止されます。

この 1 つの問題だけに対処するAFTER INSERT OR DELETE ... FOR EACH ROW EXECUTE PROCEDUREには、ユーザー項目テーブルにトリガーを作成します。このトリガーを持っています:

BEGIN
    -- Lock the user so only one tx can be inserting/deleting items for this user
    -- at the same time
    SELECT 1 FROM user WHERE user_id = <the-user-id> FOR UPDATE;

    IF TG_OP = 'INSERT' THEN
        IF (SELECT count(user_item_id) FROM user_item WHERE user_item.user_id = <the-user-id>) > 100 THEN
            RAISE EXCEPTION 'Too many items already owned, adding this item would exceed the limit of 100 items';
        END IF;
    ELIF TG_OP = 'DELETE' THEN
       -- No action required, all we needed to do is take the lock
       -- so a concurrent INSERT won't run until this tx finishes
    ELSE 
        RAISE EXCEPTION 'Unhandled trigger case %',TG_OP;
    END IF;
    RETURN NULL;
END;

または、商品所有権レコードを追加または削除する前に、顧客 ID を行レベルでロックすることにより、Rails アプリケーションに同じことを実装することもできます。私はこの種のことをどこかに適用することを忘れないトリガーで行うことを好みますが、アプリ レベルで行うことを好むかもしれないことを認識しています。悲観的ロックを参照してください。

楽観的ロックは、このアプリケーションにはあまり適していません。アイテムを追加/削除する前にユーザーのロックカウンターをインクリメントすることで使用できますが、ユーザーテーブルで行チャーンが発生するため、トランザクションがとにかく短い場合は不要です。

于 2013-04-21T05:50:09.257 に答える