热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

安全的方法增加了在railsAR中的字段值。-SafewaytoincreasefieldvalueinrailsAR

SoIvegotclassClientthathas_many:transactions.Bothfieldshasmonetizedfields(money-rails)

So I've got class Client that has_many :transactions. Both fields has monetized fields (money-rails) gem. INclass Transaction I've got after_create :add_customer_balance. It should add this transaction.amount to transaction.client balance.

我有一个类客户机,它有has_many:transactions。这两个领域都有货币化的领域(money-rails) gem。在类内事务中,我有after_create:add_customer_balance。它应该添加这个事务。事务。客户的平衡。

Problem I'm facing is situation when 2 transaction would be made in same moment. Let's have a look on this situation:

我所面临的问题是当两个交易同时发生。让我们来看看这个情况:

    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

My question is: how to prevent first situation?

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

I'm looking for solution that would increase field with balance and immediately save it to database.

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

Edit

I tried doing increment! but it seems to doesn't prevent race condition.

我试着做增量!但这似乎并不能阻止种族状况的发生。

def increment!(attribute, by = 1)
  increment(attribute, by).update_attribute(attribute, self[attribute])
end

2 个解决方案

#1


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

#2


2  

If I'm understanding your question correctly, this sounds like a textbook case for using transactions:

如果我正确地理解了你的问题,这听起来像是使用事务的教科书案例:

Transactions are protective blocks where SQL statements are only permanent if they can all succeed as one atomic action. The classic example is a transfer between two accounts where you can only have a deposit if the withdrawal succeeded and vice versa. Transactions enforce the integrity of the database and guard the data against program errors or database break-downs. So basically you should use transaction blocks whenever you have a number of statements that must be executed together or not at all.

事务是保护块,只有当SQL语句可以作为一个原子操作成功时,它们才会是永久性的。典型的例子是两个账户之间的转账,如果取款成功,你就可以有存款,反之亦然。事务执行数据库的完整性,并保护数据不受程序错误或数据库故障的影响。因此,当你有许多必须一起执行或者根本不需要执行的语句时,你应该使用事务块。

Active Record supports transactions, you can read more about them here.

活动记录支持事务,您可以在这里阅读有关事务的更多信息。

Here is the example from the documentation:

以下是文件中的例子:

ActiveRecord::Base.transaction do
  david.withdrawal(100)
  mary.deposit(100)
end

In this case, if the withdrawal from david's account fails, the deposit to mary's account is not executed. Likewise if the withdrawal succeeds and the deposit fails, the withdrawal is rolled back and no action is taken. Either everything works or nothing does, and it happens as an atomic operation - meaning nothing else can access the database before the transaction finishes (either success or failure)

在这种情况下,如果从大卫的账户中支取失败,则不执行对玛丽账户的押金。同样,如果支取成功且存款失败,支取将被回滚,并且不会采取任何行动。要么一切正常,要么什么都不做,而且它作为一个原子操作发生——这意味着在事务完成之前,没有其他任何东西可以访问数据库(无论是成功还是失败)


推荐阅读
author-avatar
戴耳机聆听世界
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有