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

Ruby on rails 请求之间的Rails实例变量冲突,ruby-on-rails,thread-safety,mutex,puma,Ruby On Rails,Thread Safety,Mutex,Puma,我有一组价格:@price\u队列 它作为Prices.find(1).price\u list保存在PostgreSQL中,并作为种子 当交易启动时,该交易采用@price_队列中的下一个价格,并被发送到支付处理器以获取费用 def create price_bucket = Prices.find(1) price_bucket.with_lock do @price_queue = price_bucket.price

我有一组价格:@price\u队列

它作为Prices.find(1).price\u list保存在PostgreSQL中,并作为种子

当交易启动时,该交易采用@price_队列中的下一个价格,并被发送到支付处理器以获取费用

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_队列

  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(在条带仪表板上确认)

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

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

期望值:

price_queue = [101, 102, 103, 104, ...]
假设尚未发生取消移位

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上用彪马

我尝试将价格存储在会话[:price]cookie[:price],将其分配给局部变量price,但没有效果

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


任何帮助都将不胜感激。(也可以随意批评我的代码)

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

  • 请求1从数据库获取价格(数组为[100101102])
  • 请求2从数据库获取价格(数组为[100101102]-一个单独的副本)
  • 请求1锁定价格、删除价格并保存
  • 请求2锁定价格、删除价格并保存
这里重要的是,请求2使用的是一个旧的price副本,它不包括请求1所做的更改:两个实例都将从数组中移出相同的值(请求可能是同一个puma worker上的不同线程、不同的worker甚至不同的dynos,这不重要)

另一个失败场景是

  • 请求1获取价格,删除价格并保存。数据库中的数组是[101102103,…],内存中的数组是[101102103,…]
  • 请求2获取价格,删除价格并保存。数据库中的数组是[102103,…]
  • 请求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
此代码与原始代码之间的主要区别在于:

  • 我获取一个锁定的行并更新它,而不是获取一行然后锁定它。这消除了我概述的第一个场景中的窗口
  • 在我释放锁后,我不会一直使用同一个活动记录对象-一旦释放锁,其他人可能会在你背后更改它
您也可以通过乐观锁定(即没有显式锁定)来实现这一点。唯一能改变代码的是

    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\u version
,以使其工作


哪一个性能更好取决于您所经历的并发程度、对该表的其他访问等等。就我个人而言,我会默认乐观锁定,除非我有令人信服的理由这样做。

现在没有时间给出正确的答案,但您的援救条款可能会覆盖其他人对价格所做的更改instances@FrederickCheung请详细说明你什么时候有时间我已经删除了我的答案,因为条纹仪表板上清楚地显示了100,发送了101102个值。在仪表板上,前两个失败了,第三个成功了?是的,没错@SteveTurczynAh,当然,这是有道理的。更清楚。干杯第一个场景在1000多个测试中尚未出现。我正在使用悲观锁定。正如我提到的,无论是失败还是成功,没有两个支付交易是相同的,因此
移位
,以及条带收费都正常运行。当将失败交易的价格重新放入数组时,问题就会出现@Frederickcheung尽管如此,我很快会给它一个机会,然后带着它回来news@shiva我解释过,当您将失败的事务放回原处时,您会面临覆盖其他事务已完成的事务的风险,问题是在取消移动之前队列没有被重新加载。我按照建议用锁实现了它。谢谢你的帮助@Frederickhung