In Ruby on Rails, dealing with concurrent updates to the same database record can lead to serious problems, like lost updates or corrupted data. One common solution is using pessimistic locking, which prevents other processes from modifying a record until the current process has finished working with it.


Real-Life Example: Inventory Management in an Online Store

Imagine an online store where customers can purchase items. Each item has a limited stock. Here’s a simplified database schema:

class Product < ApplicationRecord
  # attributes: name, stock_quantity
end

When a customer places an order, the stock quantity for the product should decrease. Here’s a simple implementation:

def purchase(product_id, quantity)
  product = Product.find(product_id)
  if product.stock_quantity >= quantity
    product.update!(stock_quantity: product.stock_quantity - quantity)
  else
    raise "Not enough stock!"
  end
end

This code works well in most cases, but it fails when multiple customers try to purchase the same product at the same time. Both processes could read the same stock_quantity before updating it, resulting in overselling.


The Solution: Pessimistic Locking

To avoid this problem, use pessimistic locks to ensure that only one process can update the stock_quantity at a time. In Rails, this can be done with the lock method.

Here’s the revised implementation:

def purchase(product_id, quantity)
  Product.transaction do
    # Lock the product record for update
    product = Product.lock("FOR UPDATE").find(product_id)

    if product.stock_quantity >= quantity
      product.update!(stock_quantity: product.stock_quantity - quantity)
    else
      raise "Not enough stock!"
    end
  end
end

How It Works

  1. Product.lock("FOR UPDATE"): This issues a SELECT ... FOR UPDATE SQL query, which locks the selected row. Other processes trying to lock the same row must wait until the current transaction completes.

  2. transaction block: Ensures that all operations are executed atomically. If any part fails, the transaction is rolled back.

  3. Concurrency Handling: While one process holds the lock, others trying to access the same record must wait. This prevents simultaneous updates to stock_quantity.


Important Notes

  • Database Support: Pessimistic locking relies on your database. Most modern databases like PostgreSQL and MySQL support it.

  • Deadlocks: Improper use of locks can cause deadlocks. Always access locked records in a consistent order to minimize this risk.

  • Performance Impact: Locks can slow down your system if many processes try to access the same record. Use them only when necessary.


Alternative: Optimistic Locking

If pessimistic locking is too restrictive for your use case, consider optimistic locking, which uses a lock_version column to detect conflicts without locking the database row. However, optimistic locking is better suited for scenarios where conflicts are rare.