Ruby on rails 请求之间的Rails实例变量冲突
我有一组价格:@price\u队列 它作为Prices.find(1).price\u list保存在PostgreSQL中,并作为种子 当交易启动时,该交易采用@price_队列中的下一个价格,并被发送到支付处理器以获取费用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
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锁定价格、删除价格并保存
- 请求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