Rails 请求之间的实例变量冲突
Rails Instance Variable Conflict between Requests
我有一组价格:@price_queue。
它作为 Prices.find(1).price_list 在 PostgreSQL 中持久存在并被播种。
发起交易时,交易采用@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。
我尝试将价格存储在 session[:price]、cookie[:price] 中,并将其分配给局部变量价格,无果。
我一直在阅读并认为这可能是由多线程环境引起的范围问题,其中@price 正在泄漏到其他控制器操作并被重新分配或变异。
如有任何帮助,我们将不胜感激。 (也可以随意批评我的代码)
这与实例变量泄漏或类似情况无关 - 只是此处发生的一些经典竞争条件。两个可能的时间表:
- 请求1从数据库中获取价格(数组为[100,101,102])
- 请求 2 从数据库中获取价格(数组是 [100,101,102] - 一个单独的副本)
- 请求 1 锁定价格,删除价格并保存
- 请求 2 锁定价格,删除价格并保存
这里重要的是请求 2 使用旧的价格副本,其中不包括请求 1 所做的更改:两个实例都将从数组中移出相同的值(请求可能是同一个 puma worker 上的不同线程,不同的 worker 甚至不同的 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
此代码与您的原始代码之间的主要区别在于:
- 我获取一个锁定的行并更新它,而不是获取一个行然后锁定它。这消除了我概述的第一个场景中的 window
- 释放锁后,我不会继续使用同一个活动记录对象 - 一旦锁被释放,其他人可能会在你背后更改它。
您也可以通过乐观锁定(即没有显式锁定)来做到这一点。唯一会改变代码的是
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
您需要添加一个名为 lock_version
的默认值为 0 的非空整数列才能正常工作。
哪个性能更好取决于您体验到多少并发性,对此 table 还有哪些其他访问等等。就我个人而言,除非有令人信服的理由,否则我会默认使用乐观锁定。
我有一组价格:@price_queue。
它作为 Prices.find(1).price_list 在 PostgreSQL 中持久存在并被播种。
发起交易时,交易采用@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。
我尝试将价格存储在 session[:price]、cookie[:price] 中,并将其分配给局部变量价格,无果。
我一直在阅读并认为这可能是由多线程环境引起的范围问题,其中@price 正在泄漏到其他控制器操作并被重新分配或变异。
如有任何帮助,我们将不胜感激。 (也可以随意批评我的代码)
这与实例变量泄漏或类似情况无关 - 只是此处发生的一些经典竞争条件。两个可能的时间表:
- 请求1从数据库中获取价格(数组为[100,101,102])
- 请求 2 从数据库中获取价格(数组是 [100,101,102] - 一个单独的副本)
- 请求 1 锁定价格,删除价格并保存
- 请求 2 锁定价格,删除价格并保存
这里重要的是请求 2 使用旧的价格副本,其中不包括请求 1 所做的更改:两个实例都将从数组中移出相同的值(请求可能是同一个 puma worker 上的不同线程,不同的 worker 甚至不同的 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
此代码与您的原始代码之间的主要区别在于:
- 我获取一个锁定的行并更新它,而不是获取一个行然后锁定它。这消除了我概述的第一个场景中的 window
- 释放锁后,我不会继续使用同一个活动记录对象 - 一旦锁被释放,其他人可能会在你背后更改它。
您也可以通过乐观锁定(即没有显式锁定)来做到这一点。唯一会改变代码的是
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
您需要添加一个名为 lock_version
的默认值为 0 的非空整数列才能正常工作。
哪个性能更好取决于您体验到多少并发性,对此 table 还有哪些其他访问等等。就我个人而言,除非有令人信服的理由,否则我会默认使用乐观锁定。