4
A transaction on your own won't help you here. The save
process is wrapped in a transaction (the before_save
, after_save
and the actual save) but even if you included the find in your transaction
单靠你自己的交易对你没有帮助。保存过程被包装在一个事务中(before_save、after_save和实际的保存),但即使您将find包含在事务中,也要这样做
Client.transaction do
client = Client.find(1)
client.balance += 100
client.save
end
Then you are still at risk. It's easy to see this by adding a random duration call to sleep
between the find
and the save
. When the save executes an exclusive lock will be acquired on the row. This would block calls to find occurring in other transactions (and so they would only see the value post save), but if the client row has already been retrieved then it won't force it to reload.
那么你仍然处于危险之中。通过在find和save之间为sleep添加一个随机持续时间调用,很容易看出这一点。当执行save时,将获取行上的独占锁。这将阻止查找在其他事务中发生的调用(因此它们将只看到值post save),但是如果客户端行已经被检索,那么它将不会强制重新加载。
There are 2 common approaches to this sort of problem
这类问题有两种常见的方法。
Pessimistic locking.
This looks like
这看起来像
Client.transaction do
client = Client.lock.find(1)
client.balance += 100
client.save
end
What this does is lock the row at the point of retrieval - any other attempt to call find
on that client will block until the end of the transaction. It's called pessimistic because even though the risk of the collision is low, you expect the worse case and lock every time. There is a performance penalty, since it blocks all attempts to read that row, even ones that weren't going to do an update. It's still the case that if this runs in parallel with
这样做的作用是在检索点锁定行——在该客户机上调用find的任何其他尝试都将被阻塞,直到事务结束。它被称为悲观,因为即使碰撞的风险很低,你也会预料到更坏的情况,每次都会锁住。存在性能损失,因为它阻止了所有读取该行的尝试,即使是不进行更新的尝试。它仍然是平行的
client = Client.find(1) #no call to lock here!
#some lengthy process
client.balance += 1
client.save
then you'll end up with bad data: the entire find-lock-update process could happen in the break between when the row was fetched and when the row was updated. Therefore all of the places where you update balance would need to use lock
然后,您将得到坏数据:整个查找锁定更新过程可能发生在从获取行到更新行之间的间隔中。因此,所有更新余额的地方都需要使用锁
Optimistic locking
With this you add a lock_version column to your model (must be of type integer and default to 0). Calls to save
will execute queries of the form
这样,您就向模型添加了一个lock_version列(必须是integer类型,默认为0)
UPDATE clients set .... lock_version = 5 where id = 1 and lock_version = 4
With each save, lock_version is incremented by 1. If no rows are updated (ie there is a mismatch on the lock_version) then ActiveRecord::StaleObjectError is raised.
对于每个保存,lock_version增加1。如果没有更新行(即lock_version不匹配),则会引发ActiveRecord::StaleObjectError。
Applying this to your example
将此应用到示例中
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
The second update will match no rows, and so the exception is raised. At this point you should reload the client object and try again.
第二个更新将不匹配任何行,因此会引发异常。此时,您应该重新加载客户端对象并再次尝试。
It's called optimistic because we assume that most of the time there won't be simultaneous updates: in the happy case the overhead is minimal. A downside is that any call to save
can result in ActiveRecord::StaleObjectError - it can be a bit of a pain handling all of those
这被称为乐观,因为我们假定大多数情况下不会同时更新:在愉快的情况下,开销很小。一个缺点是,任何要保存的调用都可能导致ActiveRecord: StaleObjectError——处理所有这些都可能有点痛苦
The documentation for these is at http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html and http://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html
这些文档在http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html和http://api.rubyonrails.org/classes/activerecord/locking/elegmistic.html