Backend Development 12 min read

Mastering Pessimistic and Optimistic Locks in Spring Boot 2.6.12

This article explains the concepts of pessimistic and optimistic locking, compares their use cases, demonstrates version‑based and CAS implementations, and shows a complete Spring Boot example with retry‑enabled optimistic lock handling using AOP and custom annotations.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Mastering Pessimistic and Optimistic Locks in Spring Boot 2.6.12

1. What Are Pessimistic and Optimistic Locks

Pessimistic lock assumes the worst case: every read acquires a lock because other threads might modify the data, causing other threads to block until the lock is released. In Java, synchronized and ReentrantLock are typical implementations.

Optimistic lock assumes the best case: reads proceed without locking, but before an update the version or CAS check ensures no other thread has changed the data. It is suitable for read‑heavy scenarios and is often implemented with a version column or java.util.concurrent.atomic classes.

2. When to Use Each Lock

Optimistic locking works well when writes are rare (high read‑to‑write ratio) because it avoids lock overhead. In write‑intensive situations, frequent conflicts cause retries, so pessimistic locking is usually more appropriate.

3. Common Optimistic‑Lock Implementations

Version‑field mechanism : add a version column to a table; the row is updated only if the version matches, otherwise the operation retries.

CAS algorithm (compare‑and‑swap) is a lock‑free technique that atomically updates a value only when it equals an expected old value, often used in spin‑retry loops.

CAS algorithm details

V – the current memory value

A – the expected value

B – the new value to write

The operation succeeds only when V equals A; otherwise it retries.

4. Practical Example

Data source configuration (application.yml)

<code>spring:
  datasource:
    driverClassName: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/testjpa?serverTimezone=GMT%2B8
    username: root
    password: xxxxxx
    ...
</code>

Entity definition

<code>@Entity
@Table(name = "t_account")
public class Account {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private BigDecimal amount = BigDecimal.ZERO;
    @Version
    private Integer version;
}
</code>

Service with deduction and recharge methods

<code>@Service
public class AccountService {
    @Resource
    private AccountDAO accountDAO;

    @Transactional
    public Account deduction(Long id, BigDecimal money) {
        Account account = accountDAO.findById(id).orElse(null);
        try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) {}
        if (account != null) {
            account.setAmount(account.getAmount().subtract(money));
            return accountDAO.saveAndFlush(account);
        }
        return null;
    }

    @Transactional
    public Account recharge(Long id, BigDecimal money) {
        Account account = accountDAO.findById(id).orElse(null);
        if (account != null) {
            account.setAmount(account.getAmount().add(money));
            return accountDAO.saveAndFlush(account);
        }
        return null;
    }
}
</code>

Note: the @Version field enables optimistic locking.

Test case demonstrating lock conflict

<code>@SpringBootTest
public class SpringBootLockRetryApplicationTests {
    @Resource
    private AccountService accountService;

    @Test
    public void testMoneyOperator() {
        CountDownLatch cdl = new CountDownLatch(2);
        Thread t1 = new Thread(() -> { accountService.deduction(1L, BigDecimal.valueOf(100)); cdl.countDown(); }, "T1 - 扣减线程");
        Thread t2 = new Thread(() -> { accountService.recharge(1L, BigDecimal.valueOf(100)); cdl.countDown(); }, "T2 - 增加线程");
        t1.start();
        t2.start();
        try { cdl.await(); } catch (InterruptedException e) {}
    }
}
</code>

The run produces an ObjectOptimisticLockingFailureException because the version changes between the two transactions.

5. Retry Mechanism via AOP

Custom retry annotation

<code>@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Retry {
    int nums() default 3; // number of retries
}
</code>

Aspect that intercepts methods annotated with @Retry

<code>@Component
@Aspect
public class PackRetryAspect {
    private static Logger logger = LoggerFactory.getLogger(PackRetryAspect.class);

    @Around("@annotation(retry)")
    public Object arround(ProceedingJoinPoint pjp, Retry retry) throws Throwable {
        int maxRetries = retry.nums();
        int attempts = 0;
        Object result = null;
        do {
            attempts++;
            try {
                result = pjp.proceed();
                return result;
            } catch (Exception e) {
                if (e instanceof ObjectOptimisticLockingFailureException || e instanceof StaleObjectStateException) {
                    logger.info("retrying....times:{}", attempts);
                    if (attempts > maxRetries) {
                        logger.info("retry exceed the max times..");
                        throw e;
                    }
                    Thread.sleep(200);
                }
            }
        } while (attempts < maxRetries);
        return result;
    }
}
</code>

Apply the annotation to the deduction method:

<code>@Retry
@Transactional
public Account deduction(Long id, BigDecimal money) { /* same implementation as before */ }
</code>

Running the test now shows the aspect retrying once and the transaction succeeding, with the version column incremented to 3 while the amount remains unchanged.

Final state: amount unchanged, version = 3.

End of tutorial.

Spring Bootoptimistic lockpessimistic lockretry mechanismJava ConcurrencyJPA
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.