Pit-falls of non-database locks in a transactional environment

Sometimes you are in a situation where you want really light-weight locks and not set pessimistic locks on the database level, aka SELECT foo FROM bar FOR UPDATE.

So, what can possibly go wrong? Well, quite a bit it turns out!

We use simple strings as lock keys. Concatenation of the entity-type + ID + operation seems trivial and easy to understand. So we could create a lock key like blogpost-122-update.

To support this basic functionality we have a Lock Manager with a simple method runWithLock(key, supplier). The functional design allows us to implement the ensured lock release in an elegant way.

public interface LockManager
{
  <T> T performWithLock(String key, Supplier<T> function);
}

The first attempt to use this was inside our DAO layer, where we need to ensure not two threads try to update the same post metadata at the same time.

So, putting it to the test! The lock does not seem to work. The entity was updated concurrently.

So, first thought, is that the transaction has not been able to commit yet.

New version of the lock function:

public interface LockManager
{
  void lockUntilEndOfTxn(String key);
}

Using a callback for when the transaction has actually committed, the lock should be held long enough that the other thread cannot bypass the lock. A bit surprised, I learn that it still is concurrently modified. After a bit of thinking I realize that the other threads’ transactions have already been initiated, i.e. with database isolation of READ COMMITTED will not see data changes after the transaction has been initiated.

OK, back to the drawing board. We need to initiate the lock outside the transaction-boundary, otherwise, we cannot get the second transaction to see the changes done by the first transaction.

public interface LockManager
{
  <T> T performWithLockAndTransaction(String key, Supplier<T> function);
}

Now the locking is working as intended. The second transaction is not started until the lock is acquired, so when it begins, the first transaction has already committed (or rolled back).

Easy, right?

Share