본문 바로가기
카테고리 없음

Transaction 격리 수준 & JPA Lock

by 걸어가는 신사 2023. 1. 29.

트랜잭션과 격리 수준

💡 동시에 여러 트랜잭션을 처리할 때, 트랜잭션이 얼마나 서로 고립되어 있는지를 의미한다. 즉, 해당 트랜잭션이 다른 트랜잭션에서 변경한 데이터를 볼 수 있는 기준을 결정하는 것이다.

READ UNCOMMITED

💡 하나의 트랜잭션이 커밋되기 전에 다른 트랜잭션이 읽을 수 있다.

  • DIRTY READ 발생
    • 다른 트랜잭션에서 처리한 작업이 완료되지 않았음에도 불구하고 다른 트랜잭션에서 볼 수 있게 되는 현상

READ COMMITTED

💡 커밋이 완료된 데이터만 트랜잭션에서 조회할 수 있다.

  • 다른 트랜잭션이 커밋하지 않은 정보는 읽을 수 없다.
  • NON-REPEATABLE READ 발생
    • 하나의 트랜잭션 내에서 동일한 SELECT 쿼리를 실행했을 때 항상 같은 결과가 다른 현상

REPEATABLE READ

💡 Undo 로그를 활용하여 NON-REPEATABLE READ 해결

  • MySQL의 InnoDB 스토리지 엔진에서 기본적으로 사용되는 격리 수준
  • Undo 영역에 백업된 모든 데이터에는 변경을 발생한 트랜잭션의 번호가 포함되어 있다.
  • REPEATABLE_READ 격리 수준에서는 실행 중인 트랜잭션보다 작은 트랜잭션에서 변경한 데이터만 볼 수 있다.

  • PHANTOM READ 발생

💡 트랜잭션 시작 시점 데이터를 읽었을 때 존재하지 않았던 데이터가 다시 같은 조건으로 데이터를 읽어 들였을 때 존재해 일치하지 않은 결과값을 반환하는 현상

  • Insert Query를 막을 수 없다.

MySQL InnoDB 스토리지 엔진에서 PHANTOM READ 해결

  • InnoDB 스토리지 엔진은 레코드 락과 갭 락을 합친 넥스트 키 락을 사용한다.
    • 레코드 락 (Record Lock)
      • 해당 레코드의 인덱스에 락을 건다.
      • 따로 생성한 인덱스가 없는 테이블은 InnoDB가 자체적으로 생성한 클러스터 인덱스를 이용해 락을 건다.
    • 갭 락 (Gap Lock)
      • 인덱스 레코드와 인접한 앞/뒤 사이 공간에 락을 건다.
  • 락을 걸어서 수정, 삽입, 삭제등의 다른 트랜잭션을 막는다.

SERIALIZABLE

💡 트랜잭션을 순차적으로 진행시키는 것

  • InnoDB 테이블에서 순수한 SELECT 작업은 아무런 레코드 Lock 없이 실행된다.
  • 하지만 SERIALIZABLE 격리 수준에서는 읽기 작업도 공유 잠금을 획득해야 한다.
  • 한 트랜잭션에서 읽고 쓰는 레코드를 다른 트랜잭션에서는 절대 접근할 수 없다.

두 번의 갱신 분실 문제

  • 트랜잭션 격리 수준으로 해결 ⇒ SERIALIZABLE로는 해결 가능
  • 두 트랜잭션이 같은 데이터를 변경했을 때, 한 트랜잭션의 결과만 남는 것을 두 번의 갱실문제라고 한다.

  • 동시에 두개의 트랜잭션이 하나의 값을 select하고 이 값을 이용하여 update 시킬 때 문제가 발생한다.
  • 두 번의 갱신 분실 문제는 데이터베이스 트랜잭션의 범위를 넘어선다.

해결방안 3가지

  • 마지막 커밋만 인정하기
    • 사용자 A의 내용은 무시하고 마지막 커밋한 사용자 B의 내용만 인정하기
  • 최초 커밋만 인정하기
    • 사용자 A가 이미 수정을 완료했으므로 사용자 B가 수정을 완료할 때 오류가 발생한다.
    • 낙관적 락이 여기에 해당한다.
  • 충돌하는 갱신 내용 병합하기
    • 사용자 A와 사용자 B의 수정사항을 병합한다.

낙관적 락 (Optimistic Lock) VS 비관적 락(Pessimistic Lock)

둘다 모두 싱글 DB에서만 가능하다.

분산 서버 환경에서 적용은 가능하다.

서비스의 비즈니스 성격 + 애플리케이션 부하 + 재시도에 대한 별도 처리

를 고려해야 한다.

낙관적 락 (Optimistic Lock) (DB 락 X, 어플리케이션 수준)

  • DB의 Lcok을 사용하지 않고 Version 관리를 통해 어플리케이션 레벨에서 처리한다.
  • 대부분의 트랜잭션이 충돌하지 않는다고 가정하는 방법이다.
  • DB의 Lock 기능을 이용하지 않고, JPA가 제공하는 버전 관리 기능을 사용한다.
  • 트랜잭션 커밋 전에는 트랜잭션 충돌을 알 수 없다.

낙관적 락을 version을 통해 관리된다, 최초 하나의 요청만 성공하고 나머지 요청들은 objectOptimisticLockingFailureException예외가 발생한다.

이체 시스템에서는 두 개의 요청이 모두 성공해야 한다. 최초 하나의 요청만 성공하고 나머지 요청에 대해서는 성공할 때까지 계속해서 요청을 보내는 로직을 구현해야 한다.

재시도에 대한 처리에 대한 코드의 복잡도가 증가한다.

애플리케이션 서버는 재시도하는 요청만큼 부하를 받게 된다.

서비스의 비

비관적 락 (DB 락 O)

  • 모든 트랜잭션은 충돌이 발생한다고 가정하고 우선 Lock을 거는 방법이다.
  • DB의 Lock 기능을 이용한다. select for update 구문을 사용하고, 버전 정보는 사용하지 않는다. (사용하도록 할 수도 있다.)
  • 엔티티가 아닌 스칼라 타입(int, String 같은 타입)을 조회할 때도 사용할 수 있다.
  • 트랜잭션 커밋하기 전에, 데이터를 수정하는 시점에 미리 트랜잭션 충돌을 감지할 수 있다.
  • Lock을 획들할 때까지 트랜잭션은 대시하므로, Timeout을 설정할 수 있다.

비관적 락(Perssimistic Lock)과 갭 락(Gap Lock)

💡 비관적 락을 사용하면 select … for update 쿼리가 발생하는데, 이 때 갭락을 사용한다.

 

JPA의 Lock 사용 방법

Lock 적용 범위

  • @Lock을 적용 가능한 범위는 아래와 같다.
    • EntityManager.lock(), EntityManage.find(), EntityManager.refresh()
    • Query.setLockMode()
    • @NamedQuery

JPA가 제공하는 Lock 옵션

낙관적 락(Optimisstic Lock) OPTIMISTIC 낙관적 락 사용
낙관적 락(Optimisstic Lock) OPTIMISTIC_FORCE_INCREMENT 낙관적 락 + 버전 정보 강제 증가
비관적 락(Pessimistic Lock) PESSIMISTIC_READ 비관적 락, 읽기 Lock 사용
비관적 락(Pessimistic Lock) PESSIMISTIC_WRITE 비관적 락, 쓰기 Lock 사용
비관적 락(Pessimistic Lock) PESSIMISTIC_FORCE_INCREMENT 비관적 락 + 버전 정보 강제 증가
기타 NONE 엔티티에 @Version이 있으면 낙관적 Lock을 적용함
기타 READ 하위 호환을 위한 것으로 OPTIMISTIC와 같음
기타 WRITE 하위 호환을 위한 것으로 OPTIMISTIC_FORCE_INVREMENT와 같음
반응형

댓글