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:

  1. http://blog.couchbase.com/optimistic-or-pessimistic-locking-which-one-should-you-pick
  2. https://4loc.wordpress.com/2009/04/25/optimistic-vs-pessimistic-locking/

Happy coding!