Locking ActiveRecord Of Rails
Situation
Data consistency is very important in many applications, especially for financial and banking applications, etc. A small error can become a tragedy if we don't take it seriously. This time, I'll talk a bit about Locking and how you can use it effectively.
Why Is Locking So Necessary?
Imagine you are building an application in which each person will have an account with with a virtual money. And the user whose id is 5 is accessing the site to buy some items, we get into this account like this:
account = Account .find_by_user_id ( 5 )
After choosing your favorite item for $ 50, click check and start paying for the item. Before making a request, we will first check if he has enough money in his account, and if he satisfies the conditions, we will then reduce his account balance. an amount corresponding to the price of the item.
1 2 3 4 5 | if account.balance> = item.price account.balance = account.balance - item.price #some other long processes here account.save end |
That seems easy, isn't it? However, if what this guy will open is a tab of the site, select another item for $ 80 and somehow click on both tabs. Although it is very rare, there may be a chance when requests on the first and second tabs to the server almost at the same time, and they are all handled by the server simultaneously. This is how the request on the first tab has been done:
1 2 3 4 5 6 7 8 9 10 11 12 13 | # account.balance = 100 account = Account.find_by_user_id (5) # item.price is 50 if account.balance> = item.price # it's good, allow user to buy this item account.balance = account.balance - item.price # account.balance is now 50 account.save end |
But after executing account.balance = account.balance – item.price and before saving to account, the CPU performs the second request (with the same code):
1 2 3 4 5 6 7 8 9 10 11 12 | account = Account.find_by_user_id (5) # account.balance is still 100 # item.price is 80 if account.balance> = item.price # it's good, allow user to buy this item account.balance = account.balance - item.price # account.balance is now 20 account.save end |
I'm sure you can see the problem now. Although after purchasing the first item, we will think that the user only has $ 50 in his account, and in theory he cannot buy another item for more than $ 50. But here, he can buy both items because it passes the condition tests.
By using Locking, we can prevent the same situation. When Locking is in place, they will not allow two processes to update objects at the same time.
In general, there are two types of Locking: Optimistic and Pessimistic . Slowly, I think you can also partly guess their true meaning.
Optimistic Locking
In this type, many users can access the same object to read its value, but if two users perform an update, there will be conflicts, only one user will succeed and one will others will not be done.
1 2 3 4 5 6 7 8 | p1 = Person.find (1) p2 = Person.find (1) p1.first_name = "Michael" p1.save p2.first_name = "should fail" p2.save # Raises a ActiveRecord :: StaleObjectError |
To create Optimistic locking , you can create a lock_version field that you want to set the lock and Rails will automatically check before updating the object. Its mechanism is quite simple. Each time the object is updated, lock_version value will be increased. Therefore, if two requests want to execute the same object, the first request will succeed because its lock_version is the same as when it is read. But the second request will fail because lock_version has been increased in the database of the first request.
In this kind of locking, you are responsible for handling the exceptions returned when updating the failed action. You can read more here:
http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html
Pessimistic Locking
With this kind of locking, only the first user to access the object will be able to update it. All other users will be excluded from even reading objects (remember that in Optimistic locking , we only lock it when updating data and users can still read it).
Rails will implement Pessimistic Locking by issuing special queries in the database. For example, suppose you want to retrieve the account object and lock it until you complete the update:
1 2 3 4 5 | account = Account.find_by_user_id (5) account.lock! #no các người dùng khác có thể đọc tài khoản này, chúng cần chờ đến khi khoá là được gỡ bỏ account.save! #lock is released, other users can read this account |
You can refer to more:
http://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html
Conclude
Locking should be used depending on the requirement. Without any special requirements, Optimistic locking is enough because it is more flexible and more concurrent requests can be served. In the case of Pessimistic Locking, you need to make sure you unlock when you finish updating the object.
Happy coding!
Libra writer