Understanding Locking in Rails ActiveRecord
Data consistency is very important in many applications, especially for application related to finance, banking, etc. A small error could become a tragedy if we do not treat it seriously. This time, I am going to talk a bit about Locking and how you can use it to protect data consistency.
Why Locking is necessary
Imagine you are building an application in which each user will have an account with a virtual money. And user with id 5 is accessing the website for buying some stuff, we retrieve the account like this:
account = Account.find_by_user_id(5)
After choosing his favorite item with the price $50, he clicks checkout and starts paying for that item. Before executing the request, we will first check if he has sufficient amount of money in his account, and if he does, we then reduce the balance in his account an amount corresponding to the price of the item.
if account.balance >= item.price
account.balance = account.balance - item.price
#some other long processes here
account.save
end
That seems easy right? However, what if this guy opens another tab of the website, chooses another item with the price $80 and somehow at the same time clicks checkout on both tabs. Although it is rare, there might be a chance when the requests on the first tab and the second come to server almost the same time, and they are both processed by the server concurrently. Let's review the code for handling the request:
#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 the account
object, CPU switches to executing the second request (with the same code)
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 am sure you can see the problem now. Although after buying the first item, we would think that user only has $50 left in his account and in theory he could not buy another item with price greater than $50. But here, he can buy both items because it passes the conditional check.
By using Locking, we can prevent similar situation. When locking is in place, they will not allow two concurrent processes to update the same object in the same time.
In general, there are two kinds of locking: Optimistic and Pessimistic. From the words, I think you can also partly guess the true meaning of them.
Optimistic Locking
In this kind of locking, multiple users can access the same object to read its value but if two users perform the conflicting update, only one user will succeed and the other one will get exception.
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
column in the model you want to place the lock and Rails will automatically check this column before updating the object. Its mechanism is pretty simple. Every time the object is updated, lock_version
value will be increased. Therefore, if two request want to perform the the same object, the first request will be successful because its lock_version
is the same as when it is read. But the second request would fail because lock_version
has already been increased in the database by the first request.
In this kind of locking, you are responsible for handling the exception returned when update operation fails. 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 accesses to the object will be able to update it. All other users will be excluded from even reading the object (remember that in Optimistic locking, we only lock it when updating the data and users are still able to read it).
Rails implement Pessimistic Locking by issuing special query in database. For example, suppose you want to retrieve account object and lock it until you finish updating
account = Account.find_by_user_id(5)
account.lock!
#no other users can read this account, they have to wait until the lock is released
account.save!
#lock is released, other users can read this account
More example is here: http://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html
Knowing which locking scheme should be used totally depends on the requirements. If it does not have any special requirements, Optimistic locking is enough because it is more flexible and more concurrent request can be served. In case of Pessimistic Locking, you need to make sure that you release the lock when you finish updating the object.
If you are interested, you can read more from other sources:
- http://blog.couchbase.com/optimistic-or-pessimistic-locking-which-one-should-you-pick
- https://4loc.wordpress.com/2009/04/25/optimistic-vs-pessimistic-locking/
Happy coding!