3

我目前正在对我正在为 Android 制作的游戏进行现场测试。这些服务是用 rails 3.1 编写的,我使用的是 Postgresql。我的一些更精通技术的测试人员已经能够通过记录他们对服务器的请求并以高并发重放它们来操纵游戏。我将尝试简要描述下面的场景,而不会陷入代码中。

  • 一个用户可以购买多个项目,每个项目在数据库中都有自己的记录。
  • 该请求转到一个控制器操作,该操作创建一个购买模型来记录有关交易的信息。
  • 贸易模型有一个方法来设置物品的购买。它基本上会执行一些合乎逻辑的步骤来查看他们是否可以购买该物品。最重要的是,他们在任何给定时间每个用户都限制为 100 个项目。如果所有条件都通过,则使用一个简单的循环来创建他们请求的项目数。

因此,他们正在做的是,通过代理记录 1 个有效的购买请求。然后以高并发重播它,这实际上是每次都允许一些额外的东西通过。因此,如果他们将其设置为购买 100 个数量,他们可以将其增加到 300-400 个,或者如果他们购买 15 个数量,他们可以将其增加到 120 个。

上面的购买方式被包裹在一个事务中。但是,即使它被包装了,它也不会在请求几乎同时执行的某些情况下停止它。我猜这可能需要一些数据库锁定。另一个需要知道的转折是,在任何给定时间,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

听起来您需要幂等请求,以便请求重放无效。在可能的情况下实施操作,以便重复它们没有效果。在不可能的情况下,给每个请求一个唯一的请求标识符并记录请求是否得到满足。您可以将请求 ID 信息保存UNLOGGED在 PostgreSQL 或 redis/memcached 的表中,因为您不需要它是持久的。这将防止一整类漏洞利用。

为了解决这个问题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;

或者,您可以在 Rails 应用程序中实现相同的功能,方法是在添加或删除任何项目所有权记录之前对客户 ID 进行行级锁定。我更喜欢在触发器中做这种事情,你不能忘记在某个地方应用它,但我知道你可能更喜欢在应用程序级别做。请参阅悲观锁定

乐观锁定不太适合此应用程序。您可以通过在添加/删除项目之前增加用户的锁定计数器来使用它,但它会导致用户表上的行流失,并且当您的事务无论如何都很短时确实没有必要。

于 2013-04-21T05:50:09.257 回答
0

除非您向我们展示您的相关架构和查询,否则我们无能为力。我想你会做类似的事情:

$ start transaction;
$ select amount from itemtable where userid=? and itemid=?;
15
$ update itemtable set amount=14 where userid=? and itemid=?;
commit;

您应该执行以下操作:

$ start transaction;
$ update itemtable set amount=amount-1 returning amount where userid=? and itemid=?;
14
$ commit;
于 2013-04-20T18:49:33.053 回答