반응형
환경: 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
  • 트랜잭션이 충돌한다고 가정하고 락을 검
  • DBMS의 락 기능을 사용(ex. SELECT FOR UPDATE)
  • 데이터 수정 시 즉시 트랜잭션 충돌여부를 확인할 수 있음
  • 조회한 레코드 자체에 락을 걸기 때문에 성능이 저하될 수 있음
PESSIMISTIC_READ
  • 반복 읽기만 하고 수정하지 않는 용도로 락을 걸 때 사용
  • 다른 트랜잭션에서 읽기는 가능함(공유 잠금)
PESSIMISTIC_WRITE
  • 일반적인 옵션. 데이터베이스에 쓰기 락
  • 다른 트랜잭션에서 읽기도 쓰기도 못함(배타적 잠금)
PESSIMISTIC_FORCE_INCREMENT
  • 다른 트랜잭션에서 읽기도 못하고 쓰기도 못함
  • 추가적으로 버저닝을 수행
  • Version 정보를 사용하는 비관적 락
  • 낙관적 락(Optimistic Lock)과 LockModeType
 
  • 트랜잭션이 충돌하지 않는다고 가정
  • 디비에 락을 걸기보다 충돌 방지에 가까움
  • JPA에서는 자체적으로 제공하는 버전 관리 기능을 사용
  • 트랜잭션을 커밋하기 전까지는 충돌 여부를 확인할 수 없음
NONE
  • Entity에 @Version이 적용된 필드만 있으면 낙관적 잠금이 적용
OPTIMISTIC
  • (READ) entity 읽기 시에도 낙관적 락 적용
  • 트랜잭션 시작 시 버전 점검이 수행되고, 트랜잭션 종료 시에도 버전 점검이 수행
  • 버전이 다르면 트랜잭션이 롤백
OPTIMISTIC_FORCE_INCREMENT
  • (WRITE)
  • 낙관적 락을 사용하면서 추가로 버전을 강제로 증가
READ
  • OPTIMISTIC과 동일
  • jpa1.0 호환용
WRITE
  • OPTIMISTIC_FORCE_INCREMENT과 동일
  • jpa1.0 호환용
 
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
반응형

+ Recent posts