-- MySQL 데드락 이력 확인SHOWENGINEINNODBSTATUS;-- "LATEST DETECTED DEADLOCK" 섹션에서 확인 가능
// 트랜잭션 A: stocks(id=1) → orders(id=1) 순서로 락// 트랜잭션 B: orders(id=1) → stocks(id=1) 순서로 락// 이 두 트랜잭션이 동시에 실행되면 데드락 발생
-- 트랜잭션 ABEGIN;SELECT*FROM stocks WHERE id =1FORUPDATE;-- stocks 락 획득-- (B가 orders 락 획득)SELECT*FROM orders WHERE id =1FORUPDATE;-- orders 락 대기 → 데드락!-- 트랜잭션 BBEGIN;SELECT*FROM orders WHERE id =1FORUPDATE;-- orders 락 획득SELECT*FROM stocks WHERE id =1FORUPDATE;-- stocks 락 대기 → 데드락!
@TransactionalpublicvoidprocessOrder(Long stockId,Long orderId){// 항상 작은 ID 먼저 → 큰 ID 순서로 락 획득Long first =Math.min(stockId, orderId);Long second =Math.max(stockId, orderId); stockRepository.findWithLock(first); orderRepository.findWithLock(second);}
@TransactionalpublicvoiddecreaseStock(Long stockId,int qty){// 락이 필요한 작업만 트랜잭션 안에 포함Stock stock = stockRepository.findWithLock(stockId); stock.decrease(qty);// 알림 전송, 외부 API 호출 등은 트랜잭션 밖에서 처리}
-- 불필요하게 넓은 범위 락 (데드락 위험 증가)SELECT*FROM orders WHEREstatus='PENDING'FORUPDATE;-- 특정 ID만 락 (범위 최소화)SELECT*FROM orders WHERE id = ? FORUPDATE;
@ServicepublicclassOrderService{@Transactional@Retryable( value =DeadlockLoserDataAccessException.class, maxAttempts =3, backoff =@Backoff(delay =100))publicvoidprocessOrder(OrderRequest request){// 데드락 발생 시 최대 3회 자동 재시도 stockRepository.decreaseStock(request.getProductId(), request.getQty());}}
# application.yml — 격리 수준 조정spring:datasource:hikari:transaction-isolation: TRANSACTION_READ_COMMITTED