일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- spring
- spring security
- 서버개발캠프
- Pessimistic Lock
- JPA
- JPA Lock
- Android
- spring security 인증
- 디자인 패턴
- 개발
- 서버
- 안드로이드
- 스마일게이트
- 스프링 log
- bean
- Transaction isolation level
- Redis
- 암호화
- JPA 동시성
- annotation
- 캠프
- JPA 비관적락
- 스프링 로그
- Inno DB
- flask
- 낙관적락 비관적락 차이
- 스프링
- JPA 낙관적락
- component
- Optimistic Lock
- Today
- Total
모르는게 많은 개발자
[JPA] 낙관적, 비관적 Lock 개념/예제 본문
이번 포스팅에서는 JPA의 낙관적 잠금과 비관적 잠금을 테스트를 통해 설명하고자 한다
포스팅 내용에 DB Lock
개념도 포함되기 때문에 Lock
에 대한 이해가 필요하면 이전 게시글(https://cjw-awdsd.tistory.com/57)을 먼저 읽고 보는 것을 추천합니다.
낙관적 잠금(Optimistic Lock)
락 처리 방법 : @Version
JPA의 @Version
어노테이션을 사용해 엔티티 버전을 관리할 수 있다. @Version
적용이 가능한 타입은 long
, integer
, short
, timestamp
이다. 아래는 어노테이션 적용 예시 코드이다.
JPA의 @Version
어노테이션을 사용하면 엔티티의 버전을 관리할 수 있다.
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Version
Integer version;
}
낙관적 락 LockMode
None
별도의 옵션을 사용하지 않아도 Entity에 @Version
이 적용된 필드만 있을 때 낙관적 락 적용
- 트랜잭션 시작후 엔티티를 수정하고 트랜잭션이 종료될 때까지 다른 트랜잭션에서 같은 엔티티가 변경되지 않음을 보장.
- 엔티티가 조회 후 변경될 때 버전도 같이 증가 → 버전이 조회 시점과 다르면 예외 발생
테스트 코드
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class LockParent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "parent_id", nullable = false)
private Long parentId;
@Version
private Integer version;
private long count;
public static LockParent ofDefault() {
return LockParent.builder().count(0L).build();
}
public void plusCount() {
this.count += 1;
}
}
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class LockTestService {
private final LockParentRepository lockParentRepository;
@Transactional
public void insertLockParent() {
LockParent lockParent = LockParent.ofDefault();
this.lockParentRepository.save(lockParent);
}
@Transactional
public void plusCount(Long parentId) {
LockParent lockParent = this.lockParentRepository.findById(parentId).orElseThrow(() -> new RuntimeException(""));
lockParent.plusCount();
}
}
@Repository
public interface LockParentRepository extends JpaRepository<LockParent, Long> {}
@SpringBootTest
public class LockTests {
@Autowired
LockTestService lockTestService;
@Test
void Parent_삽입() {
lockTestService.insertLockParent();
}
@Test
void parent_동시_업데이트() {
CompletableFuture<Void> task1 = CompletableFuture.runAsync(() -> {
this.lockTestService.plusCount(1L);
});
CompletableFuture<Void> task2 = CompletableFuture.runAsync(() -> {
this.lockTestService.plusCount(1L);
});
CompletableFuture<Void> task3 = CompletableFuture.runAsync(() -> {
this.lockTestService.plusCount(1L);
});
CompletableFuture.allOf(task1, task2, task3).join();
}
}
쿼리 순서를 보면 다음과 같다.
두개의 트랜잭션에서 SELECT
가 먼저 수행되고 UPDATE
를 처리하는 것을 볼 수 있다.
테스트 수행시 아래처럼 ObjectOptimisticLockingFailureException
발생
- 어떻게 다른 트랜잭션에서
Version
변경된 것을 체크하나
UPDATE
쿼리를 보면 WHERE
문에 verion=?
이 포함되어 있다.
트랜잭션1에서 SELECT
조회한 엔티티의 Version
을 WHERE
문에 담는다.
하지만 다른 트랜잭션2에서 만약 UPDATE
를 해서 version
이 변경되었다면 트랜잭션1의 UPDATE WHERE
문은 성립하지 않기(Version
이 달라지기에)에 변경되는 데이터가 없게 된다. Exception
확인시 Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1;
변경된 값이 없어 예외가 난 것을 확인할 수 있다.
LockMode.OPTIMISTIC
None
은 엔티티 수정시 낙관적 잠금이 발생했지만 OPTIMISTIC
은 조회에도 낙관적 잠금이 발생하도록 한다.
- 조회에도
Version
을 체크하고 트랜잭션이 종료될 때까지 다른 트랜잭션에서 변경되지 않음을 보장 dirty read
,non-repeatable read
를 방지
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class LockTestService {
private final LockParentRepository lockParentRepository;
/**
* LockParent count 증가
*/
@Transactional
public void plusCount(Long parentId) {
LockParent lockParent = this.lockParentRepository.findById(parentId).orElseThrow(() -> new RuntimeException(""));
lockParent.plusCount();
log.info("\nplusCount 종료");
}
/**
* LockParent 조회
*/
public void findParentById(Long parentId) {
this.lockParentRepository.findById(parentId);
//1초후 트랜잭션 종료
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("\nfindParentById 종료");
}
}
@Repository
public interface LockParentRepository extends JpaRepository<LockParent, Long> {
/**
* OPTIMISTIC 모드로 조회
*/
@Override
@Lock(LockModeType.OPTIMISTIC)
Optional<LockParent> findById(Long id);
}
/**
* Optimistic 테스트
*/
@SpringBootTest
public class LockTests {
@Autowired
LockTestService lockTestService;
@Test
void Lock_Optimistic_동시_조회_업데이트() {
CompletableFuture<Void> task2 = CompletableFuture.runAsync(() -> {
this.lockTestService.findParentById(1L);
});
CompletableFuture<Void> task3 = CompletableFuture.runAsync(() -> {
this.lockTestService.plusCount(1L);
});
CompletableFuture.allOf(task2, task3).join();
}
}
테스트 코드 수행후 발생한 쿼리는 다음과 같다.
findParentById()
, plusCount()
모두 트랜잭션 끝날 때 Version
을 조회한다는 것을 볼 수 있다.
결과는 아래처럼 예외가 발생한다.
최신 버전
이 발견되었다는 내용 즉, Version
을 비교할 때 값이 다르기에 예외 발생
LockMode.OPTIMISTIC_FORCE_INCREMENT
OPTIMISTIC_FORCE_INCREMENT
모드는 엔티티를 읽기만 해도 Version
을 업데이트 한다.
@Repository
public interface LockParentRepository extends JpaRepository<LockParent, Long> {
@Override
@Lock(LockModeType.OPTIMISTIC_FORCE_INCREMENT)
List<LockParent> findAll();
@Query("SELECT lp FROM LockParent lp join fetch lp.lockChildren where lp.parentId = :parentId")
@Lock(LockModeType.OPTIMISTIC_FORCE_INCREMENT)
LockParent findByIdWithChild(@Param("parentId") Long parentId);
@Query("SELECT lp FROM LockParent lp WHERE lp.parentId = :parentId")
@Lock(LockModeType.OPTIMISTIC_FORCE_INCREMENT)
LockParent findByIdForceIncrement(@Param("parentId") Long parentId);
}
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class LockTestService {
private final LockParentRepository lockParentRepository;
private final EntityManager em;
/**
* lockParent Version 증가
* lockChild Version 증가 X
*/
@Transactional
public void findParentByIdForceIncrement(Long parentId) {
LockParent lockParent = this.em.find(LockParent.class, parentId, LockModeType.OPTIMISTIC_FORCE_INCREMENT);
List<LockChild> lockChildren = lockParent.getLockChildren();
}
/**
* 모든 Parent Version 증가
*/
@Transactional
public void findAllForceIncrement() {
List<LockParent> lockParents = this.lockParentRepository.findAll();
}
/**
* fetch join LockParent Version 같이 증가
*/
@Transactional
public void findParentByIdWithChild(Long parentId) {
LockParent lockParent = this.lockParentRepository.findByIdWithChild(parentId);
}
/**
* 엔티티 값이 변경된 경우 Version 2 증가
* 값을 업데이트 하면서 한번 증가
* 트랜잭션이 끝나고 강제 한번 증가
* 총 2번
*/
@Transactional
public void updateParentForceIncrement(Long parentId) {
LockParent lockParent = this.lockParentRepository.findByIdForceIncrement(parentId);
lockParent.plusCount();
}
}
@SpringBootTest
public class LockTests {
@Autowired
LockTestService lockTestService;
@Test
void Lock_Optimistic_Force_Increment_Find_Parent() {
this.lockTestService.findParentByIdForceIncrement(1L);
}
@Test
void Lock_Optimistic_Force_Increment_Find_All() {
this.lockTestService.findAllForceIncrement();
}
@Test
void Lock_Optimistic_Force_Increment_Find_Fetch_Join() {
this.lockTestService.findParentByIdWithChild(1L);
}
@Test
void Lock_Optimistic_Force_Increment_Update() {
this.lockTestService.updateParentForceIncrement(1L);
}
}
Lock_Optimistic_Force_Increment_Find_Parent()
Parent Version
1 증가
Lock_Optimistic_Force_Increment_Find_Fetch_Join
Fetch Join
을 통해Parent
,Child
Version
1씩 증가
Lock_Optimistic_Force_Increment_Update
Version
2 증가
Lock_Optimistic_Force_Increment_Find_All
- 가져온 엔티티의 모든
Version
증가
OPTIMISTIC_FORCE_INCREMENT
사용 이유에 대한 stackoverflow
https://stackoverflow.com/questions/13581603/jpa-and-optimistic-locking-modes
비관적 잠금(Pessimistic Lock)
비관적 잠금 모드
LockModeType.PESSIMISTIC_READ
SELECT
구문에 FOR SHARE
구문을 추가하여 S-Lock
획득
아래 동시성 상황에서 DeadLock
이 발생할 수 있다.
@Repository
public interface LockParentRepository extends JpaRepository<LockParent, Long> {
@Query("SELECT lp FROM LockParent lp where lp.parentId = :parentId")
@Lock(LockModeType.PESSIMISTIC_READ)
LockParent findByIdPessimisticRead(@Param("parentId") Long parentId);
}
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class LockTestService {
private final LockParentRepository lockParentRepository;
private final EntityManager em;
/**
* PESSIMISTIC_READ 모드로 LockParent 읽은 후
* 0.5초 대기후 count Update
*/
@Transactional
public void findByParentIdPessimisticReadAndUpdate(Long parentId) {
LockParent lockParent = this.lockParentRepository.findByIdPessimisticRead(parentId);
//0.5초 대기
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
lockParent.plusCount();
}
}
@SpringBootTest
public class LockTests {
@Autowired
LockTestService lockTestService;
/**
* PESSIMISTIC_READ 모드로 LockParent 읽은 후
* 0.5초 대기후 count Update
*
* 1. Transaction1 Select For Share 구문 수행
* 2. Transaction2 Select For Share 구문 수행
* 3. Transaction1 Update 수행 (Transaction2의 Shared Lock으로 인해 대기)
* 4. Transaction2 Update 수행 (Transaction1의 Shared Lock으로 인해 대기)
* 결과: 데드락 발생
*/
@Test
void Lock_Pessimistic_Read_Find_And_Update() {
CompletableFuture<Void> task1 = CompletableFuture.runAsync(() -> {
this.lockTestService.findByParentIdPessimisticReadAndUpdate(1L);
});
CompletableFuture<Void> task2 = CompletableFuture.runAsync(() -> {
this.lockTestService.findByParentIdPessimisticReadAndUpdate(1L);
});
CompletableFuture.allOf(task1, task2).join();
}
}
아래처럼 SELECT
구문에 FOR SHARE
구문이 추가되고 DeadLock
발생하는 것을 확인할 수 있다.
Rollback
된 트랜잭션을 제외한 다른 트랜잭션 UPDATE
만 수행된다.
LockModeType.PESSIMISTIC_WRITE
SELECT
구문에 FOR UPDATE
구문을 추가하여 X-Lock
획득
PESSIMISTIC_READ
와 달리 SELECT
구문에서 X-Lock
을 획득하기에 하나의 트랜잭션에서 SELECT FOR UPDATE
가 수행되면 다른 트랜잭션에서는 SELECT FOR UPDATE
구문을 날릴 때 대기하게 된다.
@Repository
public interface LockParentRepository extends JpaRepository<LockParent, Long> {
@Query("SELECT lp FROM LockParent lp where lp.parentId = :parentId")
@Lock(LockModeType.PESSIMISTIC_WRITE)
LockParent findByIdPessimisticWrite(@Param("parentId") Long parentId);
}
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class LockTestService {
private final LockParentRepository lockParentRepository;
private final EntityManager em;
/**
* PESSIMISTIC_WRITE 모드로 LockParent 읽은 후
* 0.5초 대기후 count Update
*/
@Transactional
public void findByParentIdPessimisticWriteAndUpdate(Long parentId) {
LockParent lockParent = this.lockParentRepository.findByIdPessimisticWrite(parentId);
//0.5초 대기
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
lockParent.plusCount();
}
}
@SpringBootTest
public class LockTests {
@Autowired
LockTestService lockTestService;
/**
* PESSIMISTIC_WRITE 모드로 LockParent 읽은 후
* 0.5초 대기후 count Update
*
* 1.Transaction1 Select For Update 구문 수행
* 2.Transaction2 Select For Update 구문 수행 (Transaction1 X-Lock으로 인해 대기)
* 3.Transaction3 Select For Update 구문 수행 (Transaction1 X-Lock으로 인해 대기)
* 4.Transaction1 Update 수행(X-lock 해제)
* 5.Transaction2 Update 수행
* 6.Transaction3 Update 수행
* 결과: 3개의 트랜잭션 정상 처리
*/
@Test
void Lock_Pessimistic_Write_Find_And_Update() {
CompletableFuture<Void> task1 = CompletableFuture.runAsync(() -> {
this.lockTestService.findByParentIdPessimisticWriteAndUpdate(1L);
});
CompletableFuture<Void> task2 = CompletableFuture.runAsync(() -> {
this.lockTestService.findByParentIdPessimisticWriteAndUpdate(1L);
});
CompletableFuture<Void> task3 = CompletableFuture.runAsync(() -> {
this.lockTestService.findByParentIdPessimisticWriteAndUpdate(1L);
});
CompletableFuture.allOf(task1, task2, task3).join();
}
}
LockModeType.PESSIMISTIC_FORCE_INCREMENT
PESSMISTIC_WRITE
와 동일하게 FOR UPDATE
구문이 추가되고 더불어 OPTIMISTIC_FORCE_INCREMENT
모드처럼 버전을 사용하면서 엔티티를 읽기만 해도 Version
을 업데이트 한다
@Repository
public interface LockParentRepository extends JpaRepository<LockParent, Long> {
@Query("SELECT lp FROM LockParent lp join fetch lp.lockChildren where lp.parentId = :parentId")
@Lock(LockModeType.PESSIMISTIC_FORCE_INCREMENT)
LockParent findByIdPessimisticForceIncrement(@Param("parentId") Long parentId);
}
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class LockTestService {
private final LockParentRepository lockParentRepository;
private final EntityManager em;
@Transactional
public void findByParentByIdWithChildPessimisticForceIncrement(Long parentId) {
LockParent lockParent = this.lockParentRepository.findByIdPessimisticForceIncrement(parentId);
}
}
@SpringBootTest
public class LockTests {
@Autowired
LockTestService lockTestService;
@Test
void Lock_Pessimistic_Force_Increment_Find() {
this.lockTestService.findByParentByIdWithChildPessimisticForceIncrement(1L);
}
}
Mysql 8.0
버전부터 nowait
을 지원하는데 PESSIMISTIC_FORCE_INCREMENT
모드는 nowait
을 지원할 경우
쿼리에 nowait
포함하여 쿼리를 수행한다.
nowait
: 쿼리를 실행하며, lock
이 걸린 부분이 있다면, 기다리지 않고 실패를 시킨다.
아래는 쿼리를 동시에 3번 수행하고 nowait
에러가 발생하는 상황이다.
@Test
void Lock_Pessimistic_Force_Increment_Find() {
CompletableFuture<Void> task1 = CompletableFuture.runAsync(() -> {
this.lockTestService.findByParentByIdWithChildPessimisticForceIncrement(1L);
});
CompletableFuture<Void> task2 = CompletableFuture.runAsync(() -> {
this.lockTestService.findByParentByIdWithChildPessimisticForceIncrement(1L);
});
CompletableFuture<Void> task3 = CompletableFuture.runAsync(() -> {
this.lockTestService.findByParentByIdWithChildPessimisticForceIncrement(1L);
});
CompletableFuture.allOf(task1, task2, task3).join();
}
참고
https://willbfine.tistory.com/576?category=971447
https://hudi.blog/jpa-concurrency-control-optimistic-lock-and-pessimistic-lock/
'스프링' 카테고리의 다른 글
[스프링] 로깅 개념/설정 예제(feat. Logback) (0) | 2022.03.21 |
---|---|
[스프링] 멀티 모듈(Multi Module) 개념/예제 feat. Gradle (3) | 2021.12.19 |
[Kafka] Kafka Connect 개념/예제 (0) | 2021.10.16 |
[Spring Cloud] Eureka 개념 및 예제 (0) | 2021.09.13 |
[스프링] Validation 방법(Validator, Bean Validation) 설명/예제 (0) | 2021.08.07 |