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.
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 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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.