반응형
환경: java11, springboot 2.6.2, spring-data-jpa
DB Lock이란
- 트랜젝션 처리의 순차성을 보장하기 위한 방법입니다.
- 여러 사용자들이 같은 데이터를 동시에 접근하는 상황에서 데이터의 무결성과 일관성을 위해 데이터를 잠시 잠근다고 하여 Lock이라고 합니다.
Lock종류
- 주로 사용하는 Lock에는 낙관적 락(Optimistic Lock)과 비관적 락(Pessimistic Lock)이 있고 각각 세부 타입이 있습니다.
- 참고로 낙관적 락(Optimistic Lock)과 비관적 락(Pessimistic Lock)은 싱글 DB 환경인 경우에만 적용 가능한 개념이며, 샤딩 또는 Replication 등을 통해 DB가 분산되어 있는 환경이라면 적용할 수 없습니다.
아래 내용은 spring-data-jpa 기준으로 정리하였습니다.
- 비관적 락(Pessimistic Lock)과 LockModeType
|
PESSIMISTIC_READ |
|
PESSIMISTIC_WRITE |
|
|
PESSIMISTIC_FORCE_INCREMENT |
|
- 낙관적 락(Optimistic Lock)과 LockModeType
|
NONE |
|
OPTIMISTIC |
|
|
OPTIMISTIC_FORCE_INCREMENT |
|
|
READ |
|
|
WRITE |
|
spring-data-jpa에서 lock 사용법
사용법은 여러방법이 있지만 우리가 주로 사용하는 어노테이션 기준으로만 설명드립니다.
나머지는 아래 첨부하는 링크를 참고해주세요.
프로그램에서 lock의 사용은 아래와 같이 repository에서 사용하고자 하는 쿼리 위에 달아주시면 됩니다.
jpa에서 기본 제공해주는 findby~ 에도 작동합니다.
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT c FROM MoneyExchange c WHERE c.pk = :pk")
MoneyExchange findOneWithLock(@Param("pk") MoneyExchange.PK pk);
주의사항은 다음과 같습니다.
1. 트랜젝션 안에서만 사용 가능
만약 active한 @Transactional 이 없는 곳에서 사용하신다면 아래와 같은 익셉션을 만나실 겁니다(TransactionRequiredException).
[GIA] no transaction is in progress; nested exception is javax.persistence.TransactionRequiredException: no transaction is in progress
[Exception] no transaction is in progress; nested exception is javax.persistence.TransactionRequiredException: no transaction is in progress
2. timeout
락이 걸린 row에 접근할 경우 특정 시간을 대기하다가 LockTimeoutException을 발생합니다.
그렇다면 Timeout이 얼마로 설정되어있는지 아는게 중요하겠죠?
만약 여러분의 프로그램에 관련 설정이 없다면, 기본적으로는 DB에 세팅된 값으로 작동합니다(DB별로 지원될수도 /안 될수도 있으니 꼭 확인필요).
- mysql 5.7버전 기준. default timeout 60초(1분 뒤 에러처리)
이를 확인하는 쿼리는 아래와 같습니다.
select @@innodb_lock_wait_timeout
테스트 결과 진짜 1분 대기합니다.
//lock 이후에 sleep
>>>>>>>>sleeping
//신규 요청
2023-01-05 11:36:10 INFO [c.n.g.common.filter.RequestLogFilter .doFilter : 34][http-nio-8600-exec-2] [REQUEST] [POST] /api/external/money
...
Hibernate:
select
from
hd_moneyexchange moneyexcha0_
where
for update
2023-01-05 11:37:10 WARN [o.h.engine.jdbc.spi.SqlExceptionHelper .logExceptions : 137][http-nio-8600-exec-2] SQL Error: 1205, SQLState: 40001
2023-01-05 11:37:10 ERROR [o.h.engine.jdbc.spi.SqlExceptionHelper .logExceptions : 142][http-nio-8600-exec-2] Lock wait timeout exceeded; try restarting transaction
2023-01-05 11:37:10 DEBUG [s.t.s.AbstractPlatformTransactionManager.processRollback : 833][http-nio-8600-exec-2] Initiating transaction rollback
..
Response body: {"header":{"status":500,"message":"could not extract ResultSet; SQL [n/a]; nested exception is org.hibernate.PessimisticLockException: could not extract ResultSet","isSuccessful":false}}
그러면 별도로 설정은 어떻게 할까요?
여러가지 방법이 있습니다.
1. 프로젝트 전역 설정
spring.jpa.properties.javax.persistence.query.timeout=5000
프로퍼티 파일에 위처럼 설정하면 모든 쿼리의 락이 기본 5초로 설정됩니다(기본 단위 ms)
2. DB connection 별 설정
저희는 기본 n개의 db를 연결해서 사용합니다.
log db는 짧게 static db는 길게 설정 가능할 수도 있습니다.
#log db
log.jpa.properties.javax.persistence.query.timeout=4000
..기타 다른 설정
#static db
static.jpa.properties.javax.persistence.query.timeout=3000
..기타 다른 설정
//spring config loading 방식 이용 시
@ConfigurationProperties(prefix = "log.jpa") //여기만 다르게 추가하시면 됩니다.
public static class AJpaProperties {
private Map<String, String> properties = new HashMap<>();
public Map<String, String> getProperties() {
return properties;
}
public void setProperties(Map<String, String> properties) {
this.properties = properties;
}
}
// or 수동
Map<String,Object> properties = new HashMap();
properties.put("javax.persistence.query.timeout", 5000);
EntityManager entityManager = entityManagerFactory.createEntityManager(properties);
3. 쿼리별 설정
쿼리별로 섬세한 설정이 필요하다면 local 설정을 할 수도 있습니다.
역시 단위는 ms입니다.
local 설정은 최우선순위를 가지며 다른 전역 설정을 override합니다.
//쿼리 별 설정
//전역 설정이 있어도 override됨
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT c FROM MoneyExchange c WHERE c.pk = :pk")
@QueryHints({@QueryHint(name = "javax.persistence.lock.timeout", value = "6000")})
MoneyExchange findOneWithLock(@Param("pk") MoneyExchange.PK pk);
설정 후 로그, local timeout(6초)를 따르는 것을 확인할 수 있었습니다.
2023-01-05 13:22:07 DEBUG [s.t.s.AbstractPlatformTransactionManager.handleExistingTransa: 470][http-nio-8600-exec-2] Participating in existing transaction
select
from
hd_moneyexchange moneyexcha0_
where
for update
2023-01-05 13:22:12 WARN [com.zaxxer.hikari.pool.ProxyConnection .checkException : 182][http-nio-8600-exec-2] HikariPool-1 - Connection com.mysql.cj.jdbc.ConnectionImpl@23ae77d9 marked as broken because of SQLSTATE(null), ErrorCode(0)
com.mysql.cj.jdbc.exceptions.MySQLTimeoutException: Statement cancelled due to timeout or client request
추가적인 사항.
참고) queryHint에 위와 비슷한, 다른 옵션들도 작동할까 싶어 아래 옵션을 주었으나 무시되는 로그가 지나갔습니다. 아직 어노테이션으로 설정하는 법은 없는 듯 합니다.
- javax.persistence.lock.scope(Lock 범위 설정)
EXTENDED | OneToOne, OneToMany 등 연관관계에 있는 entity들도 Lock |
NORMAL |
해당 Entity만 lock
@Inheritance(strategy = InheritanceType.JOINED)가 선언된 Entity라면 부모도 lock
|
@QueryHints({@QueryHint(name = "javax.persistence.lock.scope", value = "EXTENDED")})
..
[http-nio-8600-exec-1] HHH000121: Ignoring unrecognized query hint [javax.persistence.lock.scope]
@QueryHints에는 안되지만 entityManager에 직접 주입 시 사용 가능합니다.
Map<String, Object> properties = new HashMap<>();
map.put("javax.persistence.lock.scope", PessimisticLockScope.EXTENDED);
entityManager.find(
Student.class, 1L, LockModeType.PESSIMISTIC_WRITE, properties);
참고)
728x90
반응형
'개발 > spring' 카테고리의 다른 글
ModelAttribute vs RequestBody data bind, 직렬화 & 역직렬화 (0) | 2023.02.21 |
---|---|
[java-springboot] 테스트 코드 종류 (0) | 2023.02.17 |
[transaction] propagation and row lock 실전편 (0) | 2023.01.05 |
[webClient] 301 move permanently (0) | 2023.01.03 |
[ssl-https] in localhost (0) | 2023.01.02 |