๐ Concurrency & Locking in Distributed Systems
Welcome back to The Code Hut Distributed Systems series! In this post, we’ll explore one of the core challenges in distributed systems: concurrency control ⚡๐ก️
๐ Why Concurrency Control Matters
In distributed systems, multiple nodes or services often need to read and write the same data concurrently. Without proper control:
- ❌ Data may become inconsistent
- ❌ Two services may overwrite each other's changes
- ❌ Unexpected errors or crashes can occur
✨ Optimistic Locking
Optimistic locking assumes conflicts are rare. Changes are made without locking, but before committing, the system checks if the data was modified by another transaction.
In Java (JPA/Hibernate), we use a @Version field:
@Entity
public class Account {
@Id
@GeneratedValue
private Long id;
private double balance;
@Version
private Long version; // Hibernate checks this on update
}
If two transactions modify the same row concurrently, Hibernate throws OptimisticLockException. We can handle this gracefully with retries ๐.
๐ Example: Retry with Optimistic Locking
int retries = 3;
boolean success = false;
while(!success && retries > 0) {
try {
transferMoney(fromId, toId, amount); // method using @Version entity
success = true;
} catch (OptimisticLockException e) {
retries--;
if(retries == 0) throw e;
}
}
๐ก️ Pessimistic Locking
Pessimistic locking assumes that conflicts are likely. When a transaction reads a record, it immediately locks it, preventing other transactions from modifying (and sometimes even reading) that data until the first transaction completes.
This approach is common in scenarios where losing updates is unacceptable — such as financial transactions or inventory updates — and ensures strong consistency ✅.
Low-Level Example (EntityManager)
Account account = em.find(Account.class, 1L, LockModeType.PESSIMISTIC_WRITE);
account.setBalance(account.getBalance() + 100);
Here, the database executes a SELECT ... FOR UPDATE under the hood.
Other transactions attempting to update the same record will wait until the lock is released, ensuring data integrity but potentially reducing concurrency ⚠️.
High-Level Example (Spring Data JPA)
Spring Data provides a cleaner way to apply pessimistic locks directly in the repository layer using @Lock.
@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT a FROM Account a WHERE a.id = :id")
Optional<Account> findByIdForUpdate(@Param("id") Long id);
}
Then in a transactional service:
@Service
public class AccountService {
@Autowired
private AccountRepository accountRepository;
@Transactional
public void deposit(Long accountId, double amount) {
Account account = accountRepository.findByIdForUpdate(accountId)
.orElseThrow(() -> new EntityNotFoundException("Account not found"));
account.setBalance(account.getBalance() + amount);
}
}
This ensures that while one transaction is updating the account, others attempting to modify it will block until the first one completes. It’s simple, declarative, and ideal for critical sections of business logic.
Tip ๐ก: You can also configure lock timeouts via @QueryHints:
@Lock(LockModeType.PESSIMISTIC_WRITE)
@QueryHints({ @QueryHint(name = "jakarta.persistence.lock.timeout", value = "3000") })
@Query("SELECT a FROM Account a WHERE a.id = :id")
Optional<Account> findByIdForUpdateWithTimeout(@Param("id") Long id);
Use pessimistic locks when correctness outweighs performance, but keep transactions short to avoid deadlocks and contention. ⚙️
✅ When to Use Which?
- ✅ Optimistic Locking: Low contention, high scalability, retries acceptable.
- ✅ Pessimistic Locking: High contention, critical operations (e.g., financial transactions).
Next in the Series ๐
In the next post, we’ll explore Consensus & Coordination ๐ค, including Raft, Paxos, and leader election in distributed systems.
Label for this post: Distributed Systems
Comments
Post a Comment