增加 Rails AR 现场价值的安全方法

Fil*_*uzi 3 ruby sql activerecord ruby-on-rails ruby-on-rails-4

所以我已经明白class Clienthas_many :transactions。这两个领域都有货币化领域 ( money-rails) gem。在class Transaction我有after_create :add_customer_balance. 它应该添加这个transaction.amounttransaction.client平衡。

我面临的问题是同一时刻进行两笔交易的情况。我们来看看这个情况:

    Variant 1:
    time / process / code 
    0:01 / P1    / client = Client.find(1)
    0:01 / P2    / client = Client.find(1)
    0:02 / P1    / client.balance += 100
    0:02 / P1    / client.save # SQL: update clients set balance = 200 where id = 1
    0:03 / P2    / client.balance += 200
    0:03 / P2    / client.save # SQL: update clients set balance = 300 where id = 1

    Variant 2
    to,e / process / code 
    0:01 / P1    / client = Client.find(1)
    0:01 / P2    / client = Client.find(1)
    0:02 / P1    / client.update_all(...) # SQL: update clients set balance = balance + 100 where id = 1
    0:03 / P2    / client.update_all(...) # SQL: update clients set balance = balance + 200 where id = 1

    Result:
    Client.find(1).balance = 400
Run Code Online (Sandbox Code Playgroud)

我的问题是:如何防止第一种情况?

我正在寻找可以增加字段平衡并立即将其保存到数据库的解决方案。

编辑

我尝试这样做 increment!,但似乎并不能阻止竞争条件。

def increment!(attribute, by = 1)
  increment(attribute, by).update_attribute(attribute, self[attribute])
end
Run Code Online (Sandbox Code Playgroud)

Fre*_*ung 5

在这里,您自己的交易对您没有帮助。该save过程包含在事务中(before_save以及after_save实际保存),但即使您将查找包含在事务中

Client.transaction do
  client = Client.find(1)
  client.balance += 100
  client.save
end
Run Code Online (Sandbox Code Playgroud)

那么你仍然处于危险之中。sleep通过在find和 之间添加随机持续时间调用,很容易看出这一点save。当保存执行时,将在该行上获取排他锁。这将阻止其他事务中发生的 find 调用(因此它们只会看到保存后的值),但如果客户端行已被检索,则不会强制它重新加载。

解决此类问题有两种常见方法

悲观锁定。

这看起来像

Client.transaction do
  client = Client.lock.find(1)
  client.balance += 100
  client.save
end
Run Code Online (Sandbox Code Playgroud)

其作用是在检索时锁定该行 - 调用find该客户端的任何其他尝试都将阻塞,直到事务结束。之所以称为悲观,是因为尽管碰撞的风险很低,但您每次都会预料到最坏的情况并锁定。存在性能损失,因为它会阻止读取该行的所有尝试,即使是那些不打算进行更新的尝试。情况仍然如此,如果这与并行运行

client = Client.find(1) #no call to lock here!
#some lengthy process
client.balance += 1
client.save
Run Code Online (Sandbox Code Playgroud)

那么你最终会得到错误的数据:整个查找锁定更新过程可能发生在获取行和更新行之间的中断期间。因此,您更新余额的所有地方都需要使用lock

乐观锁

这样,您就可以向模型中添加一个 lock_version 列(必须为整数类型,默认为 0)。调用save将执行以下形式的查询

UPDATE clients set .... lock_version = 5 where id = 1 and lock_version = 4
Run Code Online (Sandbox Code Playgroud)

每次保存时,lock_version 都会增加 1。如果没有更新行(即 lock_version 不匹配),则会引发 ActiveRecord::StaleObjectError。

将此应用到您的示例中

0:01 / P1 / client = Client.find(1) #lock_version is 1
0:01 / P2 / client = Client.find(1) #lock_version is 1
0:02 / P1 / client.balance += 100
0:02 / P1 / client.save # update clients
                        # set balance = 200, lock_version = 2 
                        # where id = 1 and lock_version = 1
0:03 / P2 / client.balance += 200
0:03 / P2 / client.save # update clients
                        # set balance = 300, lock_version =2
                        # where id = 1 and lock_version = 1
Run Code Online (Sandbox Code Playgroud)

第二次更新将不匹配任何行,因此引发异常。此时您应该重新加载客户端对象并重试。

之所以称为乐观,是因为我们假设大多数情况下不会同时更新:在满意的情况下,开销是最小的。缺点是任何调用都save可能导致 ActiveRecord::StaleObjectError - 处理所有这些可能会有点痛苦

这些文档位于http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.htmlhttp://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html