Spring JPA에서 비관적 락 (Pessimistic lock) 사용 시 Lock timeout을 함께 적용해주어야 한다. 그렇지 않으면 락을 얻기위해 무한정 대기하게 될 수 있다.
DB종류별 LockTimeout 기본값은 다음과 같다.
LockTimeout | Oracle | MySQL | PostgreSQL | H2 | Apache Derby |
unit of measurement | sec | sec | ms | ms | sec |
nowait (or similar) | 0 | 0 | 1 | 1 | 0 |
min | 0 | 0 | 1 | 1 | 0 |
max | 2.147.483 | oo (no internal limit) | 2.147.483.647 | 2.147.483.647 | oo (no internal limit) |
infinite wait | -1 | N/A | 0 | N/A | N/A |
default | -1 | 50 | 0 | 4000 | 60 |
https://blog.mimacom.com/testing-pessimistic-locking-handling-spring-boot-jpa/
- Oracle and PostgreSQL, by default, never throw a LockTimeoutException (dangerous!);
- MySQL waits up to 50 seconds by default before throwing it (too long!).
PostgreSQL의 경우 default로 무한정 대기하므로 비관적 락 사용 시 Lock timeout을 지정해주어야한다.
JPA에서 Lock timeout을 지정하는 방법으로 아래와 같이 Hint를 지정해주는 방식이 많이 소개되고 있다.
@QueryHints({
@QueryHint(name = "javax.persistence.lock.timeout", value = "2000")
})
@Query(nativeQuery = true, value = "select * from users u where u.id = :id for update")
Optional<User> findByIdForUpdate(Long id);
하지만 PostgreSQL에서는 JPA가 해당 방식을 지원하지 않는다.
jpa feature | Oracle | MySQL | PostgreSQL | H2 | Apache Derby |
LockModeType.PESSIMISTIC_WRITE | v | v | v | v | v |
javax.persistence.lock.timeout | v | ||||
javax.persistence.lock.timeout=0 (nowait) | v | v | |||
handling of LockTimeoutException | v | v | v | v |
따라서 수기로 Lock timeout을 수행해주어야한다.
Lock timeout을 사용하는 경우가 많을 것으로 예상되어, Spring AOP를 활용하여 특정 Annotation에 대해 쿼리 수행전 Lock timeout을 걸도록 개발해보자.
먼저 어노테이션을 생성하자.
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LockTimeout {
int timeout() default 5000; // 5s
}
timeout값을 받아올 수 있도록 필드도 추가하였다. (기본값 5초)
Lock timeout을 걸기 위한 Repository를 정의하자.
@Component
@RequiredArgsConstructor
public class CommonRepository {
private final EntityManager em;
public void setLockTimeout(int timeout) {
Query query = em.createNativeQuery("set local lock_timeout = " + timeout);
query.executeUpdate();
}
}
다음으로 AOP를 적용해보자.
쿼리 수행 전에 수행되도록 Before 어노테이션을 활용한다.
@Aspect
@Component
@RequiredArgsConstructor
public class LockTimeoutAspect {
private final CommonRepository commonRepository;
@Before("@annotation(com.app.config.jpa.annotation.LockTimeout)")
public void beforeLockTimeout(JoinPoint jointPoint) {
MethodSignature methodSignature = (MethodSignature) jointPoint.getSignature();
LockTimeout lockTimeout = methodSignature.getMethod().getAnnotation(LockTimeout.class);
commonRepository.setLockTimeout(lockTimeout.timeout());
}
}
이제 해당 어노테이션을 사용해보자.
비관적 락을 사용하는 쿼리에 해당 어노테이션을 사용한다.
@LockTimeout(timeout = 3000)
@Query(nativeQuery = true, value = "select * from users u where u.id = :id for update")
Optional<User> findByIdForUpdate(Long id);
이제 쿼리를 수행해보면, 쿼리 수행 전에 Lock timeout을 먼저 걸게 된다.
/* dynamic native SQL query */ set local lock_timeout = 3000;
/* dynamic native SQL query */ select * from users u where u.id = ? for update
만약 timeout 시간 전에 lock을 획득하지 못하면 PessimisticLockingFailureException이 발생하게 된다.
org.springframework.dao.PessimisticLockingFailureException: could not extract ResultSet; SQL [n/a]; nested exception is org.hibernate.PessimisticLockException: could not extract ResultSet
추후 DB Lock 이 아닌, Redis의 Redisson 분산락을 활용해 어플리케이션 레벨에서 Lock을 걸어볼 수도 있을것 같다.
댓글