How to Prevent and Handle Database Deadlocks in Spring Boot 3
Learn how Spring Boot 3 handles database deadlocks by exploring transaction isolation levels, optimistic and pessimistic locking, and practical code examples that show prevention, detection, and retry strategies to keep your applications reliable under high concurrency.
When multiple transactions concurrently access the same data, concurrency can cause deadlocks in Spring Boot applications. This article explains the mechanisms of database deadlocks, transaction isolation levels, optimistic and pessimistic locking, and provides practical Spring Boot code examples for prevention, detection, and handling of deadlocks.
1. Introduction
Concurrent transactions that lock the same resources may lead to deadlocks, where each transaction waits for the other to release a lock, creating a cycle that halts progress. Understanding transaction isolation, lock types, and how databases manage concurrency is essential.
Transaction and Lock Types
A transaction is a series of database operations that must all succeed or all be rolled back, ensuring ACID properties. Databases use locks—shared (read) and exclusive (write)—to enforce isolation. Locks can be applied at row, page, or table level.
Deadlock Mechanism
Deadlocks occur when transactions acquire locks in a circular order, e.g., Transaction A locks row 1 then requests row 2, while Transaction B locks row 2 then requests row 1.
Transaction A locks row 1, then tries to lock row 2.
Transaction B locks row 2, then tries to lock row 1.
Both wait indefinitely, causing a deadlock.
2. Practical Cases
2.1 Update Stock Example
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Transactional
public void updateStock(Long productId, int quantity) {
Product product = productRepository.findById(productId).orElseThrow();
product.setStock(product.getStock() - quantity);
productRepository.save(product);
}
}If two users update the same product simultaneously, the operations may block rather than deadlock unless multiple resources are locked in conflicting order.
2.2 Choose Appropriate Isolation Level
Isolation levels define how transactions interact. Common levels:
READ COMMITTED – prevents reading uncommitted data.
REPEATABLE READ – guarantees consistent reads within a transaction.
SERIALIZABLE – executes transactions sequentially, eliminating conflicts but reducing performance.
In Spring Boot you can set the level with @Transactional(isolation = Isolation.READ_COMMITTED):
@Service
public class OrderService {
@Transactional(isolation = Isolation.READ_COMMITTED)
public void processOrder(Long orderId) {
// ...
}
}2.3 Optimistic Lock
Optimistic locking adds a version field to detect concurrent updates without holding locks.
@Entity
@Table(name = "s_product")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Version
private Integer version;
private String name;
private Integer stock;
// getters, setters
}If the version has changed, Hibernate throws OptimisticLockException, prompting a retry.
2.4 Pessimistic Lock
Pessimistic locking forces exclusive access until the transaction completes.
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<Product> findById(Long id);
}Use it when data loss is unacceptable, such as financial transactions.
2.5 Detect and Handle Deadlocks in Spring Boot
Even with precautions, deadlocks may occur. Databases typically throw a specific exception that can be caught and handled.
public interface UserRepository extends JpaRepository<User, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<User> findById(Long id);
}
@Transactional
public void updateUser(Long id) {
try {
this.userRepository.findById(id);
// other operations
} catch (Throwable e) {
e.printStackTrace();
}
}Example MySQL command line showing a deadlock:
2.6 Exponential Backoff Retry
Retrying with a delay reduces contention.
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Transactional
public void updateUser(Long id) {
try {
this.userRepository.findById(id);
// other operations
} catch (PessimisticLockingFailureException e) {
System.err.printf("发生异常");
retryTransaction(id);
}
}
private void retryTransaction(Long id) {
int retryCount = 0;
int maxRetries = 3;
while (retryCount < maxRetries) {
try {
updateUser(id);
return;
} catch (PessimisticLockingFailureException e) {
retryCount++;
long waitTime = (long) Math.pow(2, retryCount) * 100;
System.out.println("Retrying transaction after " + waitTime + "ms...");
try {
TimeUnit.MILLISECONDS.sleep(waitTime);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
break;
}
}
}
}
}This approach avoids immediate retries, reduces competition, and improves the chance of successful transaction completion without triggering additional deadlocks.
For more details, refer to the linked articles on Spring Boot transaction management, optimistic/pessimistic locking, and high‑concurrency best practices.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
