请求之间的Rails实例变量冲突

时间:2015-02-06 19:22:11

标签: ruby-on-rails thread-safety mutex puma

我有一系列价格:@price_queue。

它在PostgreSQL中作为 Price.find(1).price_list 保留并播种。

当启动交易时,交易将在@price_queue中获取下一个价格,然后将其发送到付款处理器以进行收费。

def create

  price_bucket = Prices.find(1)

  price_bucket.with_lock do                          
    @price_queue = price_bucket.price_list         
    @price = @price_queue.shift                    
    price_bucket.save                              
  end

      customer = Stripe::Customer.create(
          :email        => params[:stripeEmail],
          :card         => params[:stripeToken],
      )
      charge = Stripe::Charge.create(
          :customer     => customer.id,
          :amount       => @price * 100,
      )

      if charge["paid"]
        Pusher['price_update'].trigger('live', {message: @price_queue[0]})
      end

如果交易成功,它应该摆脱它持有的@price。 如果失败,价格应该放回@price_queue。

  rescue Stripe::CardError => e
    flash[:error] = e.message
    @price_queue.unshift(@price)
    Pusher['price_update'].trigger('live', {message: @price_queue[0]})
    price_bucket.price_list = @price_queue
    price_bucket.save
    redirect_to :back
  end

我在测试时发现了一个主要的错误,以毫秒为间隔,两次失败的交易,然后是一次通过。

price_queue = [100, 101, 102, 103, ...]

用户1获得100(在Stripe仪表板上确认)

用户2获得101(在Stripe仪表板上确认)

用户3获得102(在Stripe仪表板上确认)

预期:

假设尚未发生任何非移位

price_queue = [103, 104, ...]

用户1失败,退回100

price_queue = [100, 103, ...]

用户2失败,将101放回

price_queue = [101, 100, 103, ...]

用户3通过,102消失

真正发生的事情:

price_queue = [101, 102, 103, 104, ...]

正如我们所看到的,100正在消失,虽然它应该回到队列中,101又回到队列中(很可能不是预期的行为),并且102正被放回队列中,即使它不应该甚至可以穿越救援路径。

我在Heroku上使用Puma。

我已尝试将价格存储在会话[:价格] Cookie [:价格] 中,并将其分配给本地变量价格 ,无济于事。

我一直在阅读并认为这可能是多线程环境导致的范围问题,其中@price正在泄漏给其他控制器操作并被重新分配或变异。

非常感谢任何帮助。 (也可以随意批评我的代码)

1 个答案:

答案 0 :(得分:1)

这与实例变量泄漏或类似事件无关 - 这里只是一些经典竞争条件。两个可能的时间表:

  • 请求1从数据库中获取价格(数组为[100,101,102])
  • 请求2从数据库中获取价格(数组为[100,101,102] - 单独的副本)
  • 请求1锁定价格,删除价格并保存
  • 请求2锁定价格,删除价格并保存

重要的是,请求2使用的是价格的旧副本,不包括请求1所做的更改:两个实例都会将相同的值从数组中移出(请求可能是不同的线程在同一个美洲狮工人,不同的工人,甚至不同的dynos - 无所谓)

另一种失败的情况是

  • 请求1提取价格,删除价格并保存。数据库中的数组是[101,102,103,...],内存数组是[101,102,103,...]
  • 请求2提取价格,删除价格并保存。数据库中的数组是[102,103,...]
  • 请求2的条带事务成功
  • 请求1的条带事务失败,因此它将100放回阵列并保存。因为您没有从数据库重新加载,所以会覆盖来自请求2的更改。

为了正确解决这个问题,我将拆分逻辑以获取并将价格替换为自己的方法 - 比如

class Prices < ActiveRecord::Base

  def with_locked_row(id)
    transaction do
      row = lock.find(id)
      result = yield row
      row.save #on older versions of active record you need to tell rails about in place changes
      result
    end

  def self.get_price(id)
    with_locked_row(id) {|row| row.pricelist.shift}
  end

  def self.restore_price(id, value)
    with_locked_row(id) {|row| row.pricelist.unshift(value}
  end
end

然后你做

  Prices.get_price(1) # get a price
  Prices.restore_price(1, some_value) #put a price back if the charging failed

这与原始代码的主要区别在于:

  • 我获取一个锁定行并更新它,而不是获取一行然后锁定它。这消除了我概述的第一个场景中的窗口
  • 在我释放锁之后,我没有继续使用相同的活动记录对象 - 一旦锁被释放,其他人可能会在背后更换它。

您也可以通过乐观锁定(即没有显式锁定)来完成此操作。唯一会改变代码的方法是

    def with_locked_row(id)
      begin
        row = lock.find(id)
        result = yield row
        row.save #on older versions of active record you need to tell rails about in place changes
        result
      rescue ActiveRecord::StaleObjectError
        retry
      end
    end

您需要添加一个非空整数列,其默认值为0,名为lock_version,以便工作。

哪个性能更好取决于您经历的并发性,此表的其他访问权限等等。就个人而言,我会默认乐观锁定,除非我有其他令人信服的理由。