안녕하세요!! 이번 게시물은 저번 시간에 이어서 동시성 이슈처리를 위한 방안에 대해 알아보려고 합니다.
이번엔 트랜잭션 격리수준에 대해 중점적으로 알아보려고 하는데요,,
동시성 이슈는 결국 여러 트랜잭션이 동시에 작업을 처리하게 되면서 생기는 문제이기 때문에
트랜잭션 격리수준을 다르게 가져가면 해결해볼 수 있지 않을까? 라는 아이디어가 떠올랐습니다.
그럼 먼저 4가지 트랜잭션 격리수준에 대해 알아보겠습니다.
일관성(Consistency)이 높은 순서대로 설명드리겠습니다.
1. SERIALIZABLE (직렬화 가능한)
- 설명: 데이터에 접근할 때 기본적으로 s-lock을 획득한다. 쓰기 작업을 위해 x-lock을 획득해야 하므로 특정 데이터에 대해 조회 중인 다른 트랜잭션이 존재하면 쓰기 작업이 불가능하다.
- 예상 문제: 성능 저하 & 데드락 위험 높음
SERIALIZABLE은 일관성이 가장 높은 격리수준입니다.
SERIALIZABLE에선 하나의 트랜잭션이
특정 로우를 SELECT로 접근을 할 때 기본적으로 락(공유락)을 거는 상태로 접근하게 됩니다.
따라서 SERIALIZABLE의 조건에서
A트랜잭션이 SELECT 하고 있는 특정 로우에 대해서 (s-lock이 걸린 상태)
B트랜잭션은 읽기만 가능하고 쓰기 아예 불가능합니다. (쓰기가 가능하려면 다른 트랜잭션의 s-lock이 해제되어야함)
이 격리조건은 일관성이 가장 높은 격리수준이지만 성능이 좋지 않으며 데드락의 위험이 높은 트랜잭션 격리수준입니다.
다른 격리수준의 이슈(Phantom Read)를 방지할 목적 외에는 잘 사용하지 않는다고 합니다.
cf) 데이터베이스 락 - 공유락 vs 배타락
- 공유락(읽기락 s-lock)
읽기락이 걸린 로우에 대해 다른 트랙잭션은 읽기(SELECT)만 가능하며, 쓰기 불가.
공유락이 걸린 로우에 대해 다른 트랜잭션도 공유락 획득 가능 하나, 배타락은 획득 불가 -> 데드락 주의
공유 락을 사용하면, 하나의 트랜잭션 안에서 조회한 데이터가 다른 트랜잭션에 의해 변경되지 않음을 보장한다.
- 배타락(쓰기락 x-lock)
특정 로우에 대해 배타락을 획득한 트랜잭션은 읽기와 쓰기 모두 가능함.
배타락이 걸려있는 상태의 로우는 다른 트랜잭션이 접근하여 읽기나 쓰기를 수행 불가.
배타락이 걸려있다면 다른 트랜잭션은 공유락, 배타락 둘 다 획득 할 수 없다.
2. REPEATABLE READ (반복 가능한 읽기)
- 설명: 트랜잭션 내에서 동일한 조회 쿼리를 실행했을 때 같은 결과를 보장함
- 예상 문제: Phantom Read (언두로그 사용안하는 락으로 읽을 경우)
그 다음 트랜잭션 격리수준은 REPEATABLE READ입니다.
이는 SERIALIZABLE에 이어서 두번째로 일관성이 높은 격리수준을 갖고 있으며 MySql의 기본설정인 트랜잭션 격리수준입니다.
REPEATABLE READ은 기본적으로 하나의 트랜잭션에서 동일한 조회 쿼리에 같은 결과를 보장하는 격리수준입니다.
격리수준은 첫 조회의 결과에 해당하는 언두로그의 스냅샷을 가지고 작업을 진행하기 때문에, 다른 트랜잭션이 변경 및 커밋한 로우에 대해서도 변경 전과 동일한 조회 결과를 가져옵니다.
참조) 언두로그란?
하지만 일반적이지 않는 작업(lock을 통해 조회하는 경우 언두로그에 lock을 걸 수 없으므로 실제 데이터에 대해 작업을 실행함)
의 경우처럼 언두로그의 스냅샷을 조회하는 것이 아닌, 실제 변경된 데이터 자체를 조회하게 된다면 기존 조회 값과 다른 값을 가져오는 팬텀리드(Phantom Read)가 발생할 수 있습니다.
또한, SERIALIZABLE 트랜잭션 격리수준이 아니더라고 다른 격리수준에서도 데드락에 대한 위험은 존재하므로 이를 주의하여야합니다.
참조 ) REPEATABLE READ에서의 데드락
3. READ COMMITTED (커밋된 읽기)
- 설명: 다른 트랜잭션이 커밋한 데이터만 읽을 수 있음. 읽기 동안에는 다른 트랜잭션에 의해 변경된 데이터는 읽히지 않음.
- 예상 문제: Non-repeatable Read
그 다음으로 낮은 일관성을 갖고 있는 트랜잭션 격리수준은 READ COMMITTED입니다. 오라클 등의 데이터 베이스에서 기본 트랜잭션 격리수준으로 설정하고 있어 실무에서 자주 사용되고 있는 격리수준입니다. 커밋된 트랜잭션만 읽을 수 있기 때문에 기본적인 일관성을 획득할 수 있고 앞서 설명드린 격리수준에 비해선 일관성이 떨어지는 반면, 성능이 빠른 편입니다.
하지만 REPEATABLE READ에서 발생했었던 팬텀리드와 더불어 NON-REPEATABLE READ의 이슈를 갖고 있습니다.
NON-REPEATABLE READ란 다른 트랜잭션에서 발생시킨 커밋 때문에 하나의 트랜잭션에서 같은 쿼리로도 다른 결과를 조회할 수 있는 이슈입니다. 커밋되었다면 조회 가능하도록 하는 것이 이 격리수준의 목표이므로 NON-REPEATABLE READ는 어찌보면 당연한 결과로 볼 수 있겠습니다.
4. READ UNCOMMITTED (미결된 읽기)
- 설명: 다른 트랜잭션에 의해 변경된, 아직 커밋되지 않은 데이터도 읽을 수 있음.
- 예상 문제: Dirty Read
READ UNCOMMITTED 격리수준은 트랜잭션 격리 수준 중에 가장 낮은 일관성을 갖고 있습니다.
그 이유는 다른 트랜잭션에 의해 변경되었지만, 아직 커밋이 되지도 않은 데이터를 읽을 수 있기 때문입니다.
팬텀리드와 NON-REPEATABLE READ의 문제를 갖고 있으면서도
커밋되지 않은 데이터를 읽는 Dirty Read(더티 리드) 이슈를 갖고 있기 때문에
READ UNCOMMITTED 격리수준은 일관성을 확보하기 매우 어려워서 일반적으로 사용되기 어렵습니다.
위에서 4가지 트랜잭션 격리수준을 알아보았습니다.
과연 트랜잭션 격리수준 설정으로 우리의 목표인 동시성 요청 이슈를 해결할 수 있을까요?
일관성이 낮은 READ UNCOMMITTED와 READ COMMITTED는 약간만 생각하더라도 동시성 요청에 이슈 해결엔 부적합해보입니다. Point엔티티의 points에 대한 계산 과정 중에 다른 트랜잭션에 의한 기존 데이터 값이 도중 변경되어버릴 수 있는 가능성이 매우 높아 보입니다.
REPEATABLE READ 또한 동일 조회 결과를 보장하는 격리수준으로는 동시성 요청엔 효과가 없을 듯해 보입니다. 이슈를 해결하기 위해선, 동시에 변경되는 작업에 대한 일관성이 보장되어야합니다.
그럼 일관성이 가장 높은 격리 수준인 SERIALIZABLE의 경우는 어떨까요?
하나의 트랜잭션이 s-lock을 통해 접근하고 쓰기 작업이 진행될 경우 x-lock을 획득하고 데이터를 변경해야하니 이론적으로 그럴싸해 보입니다.
@Transactional(isolation = Isolation.SERIALIZABLE)으로 트랜잭션 격리수준을 직렬화 가능 단계로 설정하였습니다.
@Transactional(isolation = Isolation.SERIALIZABLE) // 직렬화 가능 트랜잭션 격리수준
public void subtractPoint(Long id, Long point){
Point post = findPost(id); // 접근 시 s-lock 획득
if(post.getPoints() - point >= 0){ // x-lock을 획득하고 쓰기 작업 진행
post.substractPoint(point);
}
log.info("PointService.subtractPoint()");
}
테스트 결과는 어떻게 되었을까요?
두둔~
데드락이 발생하여 테스트가 실패하였습니다.
어찌된 일일까요?
데드락의 발생 원인은 s-lock에서 상태에서 x-lock 획득 중에 발생하였습니다.
SERIALIZABLE 트랜잭션 격리수준은 모든 SELECT 작업에 대해서 s-lock을 획득합니다.
따라서 각기 다른 트랜잭션이 조회를 동시에 한다면 동시에 s-lock을 얻은 상태입니다.
이 때 각 트랜잭션이 쓰기 작업을 위해 x-lock 획득을 시도합니다.
x-lock은 다른 트랜잭션의 s-lock이 제거되고 나서야 획득 가능하므로 각 트랜잭션은 무한히 대기를 하게 됩니다...
이렇게 동시에 트랜잭션이 서로의 종료를 대기하게 되면서 더 이상 작업이 진행되지 않는 데드락이 발생하게 되는 것 입니다..!
실제 실무에서도 동시성 이슈를 해결하기 위해 SERIALIZABLE을 사용하는 경우는 없다고 합니다.
(주로 REPEATABLE READ이하의 Phantom Read를 해결하기 위해서 SERIALIZABLE 사용)
그렇다면 .. 동시성 이슈 ..
도대체 어떻게 해결해야할까요..?
트랜잭션 격리수준으로도 해결하지 못한 동시성 이슈...
다음 게시물에 이어서 찾아보도록 하겠습니다!
참고)
https://tecoble.techcourse.co.kr/post/2023-08-16-concurrency-managing/
https://hudi.blog/troubleshooting-of-category-role-concurrency-issue/
https://mangkyu.tistory.com/299
https://mangkyu.tistory.com/299
https://hudi.blog/mysql-8.0-shared-lock-and-exclusive-lock/
https://jyeonth.tistory.com/32
'백엔드 > Java' 카테고리의 다른 글
동시성 이슈 처리 및 테스트 방안 4 - 락 (2) | 2024.04.29 |
---|---|
동시성 이슈 처리 및 테스트 방안 - 2(CountDownLatch 활용) (6) | 2024.03.25 |
동시성 이슈 처리 및 테스트 방안 - 1 (6) | 2024.02.26 |
천사와 악마 사이 추상클래스... - 인터페이스 및 정적 유틸리티 클래스 비교 - 2 (5) | 2024.02.13 |
천사와 악마 사이 추상클래스... - 인터페이스 및 정적 유틸리티 클래스 비교 - 1 (0) | 2024.02.01 |