我有一系列价格:@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正在泄漏给其他控制器操作并被重新分配或变异。
非常感谢任何帮助。 (也可以随意批评我的代码)
答案 0 :(得分:1)
这与实例变量泄漏或类似事件无关 - 这里只是一些经典竞争条件。两个可能的时间表:
重要的是,请求2使用的是价格的旧副本,不包括请求1所做的更改:两个实例都会将相同的值从数组中移出(请求可能是不同的线程在同一个美洲狮工人,不同的工人,甚至不同的dynos - 无所谓)
另一种失败的情况是
为了正确解决这个问题,我将拆分逻辑以获取并将价格替换为自己的方法 - 比如
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
,以便工作。
哪个性能更好取决于您经历的并发性,此表的其他访问权限等等。就个人而言,我会默认乐观锁定,除非我有其他令人信服的理由。