본문 바로가기
일상/회고

Repeatable read 의 Phantom read와 Consistent read의 문제점

by 계범 2023. 8. 8.

상황

더보기

타 셀 동기로부터 @Async 사용하는 메소드의 비동기 처리가 끝나길 기다렸다가 응답 받는 방법이 있냐는 질문을 받았다.

 

그래서 CompletableFuture로 return받으라고 전달했는데, 해당 내용으로 해결되진 않는다고 했다.

 

상황을 파악해보니,
동기가 처한 상황은 부모 스레드에서 트랜잭션을 연 후, @Async를 통해 돌게 된 자식 스레드의 메서드 내에서 @Transactional(propagation = Propagation.REQUIRES_NEW) 을 통해 트랜잭션을 열어서 데이터를 저장 후 커밋하였고, 부모 스레드에서 db 조회 시에 조회가 되지 않는단 이슈였다..!

 

즉, 트랜잭션을 열고 다른 트랜잭션에서 데이터를 인서트한 커밋내용이 기존에 열어둔 트랜잭션에서 조회 시 보여야하는 것이다.(phantom read)

 

해결 과정

첫번째 의심 영속성

더보기

일단 첫번째 의심한건 부모 트랜잭션 단에서 조회를 통한 영속성에 들어 간 후,
자식 스레드 단에서 트랜잭션 새로 열어서 커밋했기 때문에 기존 영속성 1차 캐시를 바탕으로 조회하는건가 싶었다...

 

근데 이건 부모 스레드에서 조회를 하지 않았기 때문에 영속성에 들어가지 않았을 거라고했고,
where 절로 id값이 아닌 다른 걸 조건으로 줘서 조회한다고 하면서
1차캐시는 id를 바탕으로한 조회 시에 사용될거라고 전해줬다..

 

JPA 1차 캐시에 대해 다시 공부해보니 아래와 같으므로 만약 없었으면 db에 재조회를 했어야한다...
1) 1차 캐시는 키,밸류 형태로 id - entity 로 묶여 있다.

2) 그러므로 id 값을 바탕으로 한 조회 시에 1차 캐시가 사용된다.

3) 1차 캐시는 해당 조회 값이 매칭되면 1차캐시에서 쓰이고, 없으면 db에 조회한다.

두번째 의심 트랜잭션 고립수준

우리는 mysql innoDB 엔진을 쓰고 있었고, default 트랜잭션 고립수준은 repeatable read다!

결론만 말하면, 동기가 원하는 건 phantom read 현상이였던 거다..!

 

repeatable read란?

트랜잭션 고립수준의 level 2 단계로,
mysql innoDB의 default 레벨이다.

 

repeatable read는 반복가능한 읽기란 뜻으로,

한 트랜잭션 내에서 같은 데이터를 여러번 읽을 때 그 데이터는 시작 시점에서의 상태와 동일성을 보장해주는 수준이다.

트랜잭션에서 데이터를 읽는 동안 읽기 잠금을 설정하여 다른 트랜잭션이 해당 데이터를 변경하지 못하게 보장한다.

 

하지만 phantom read 현상이 발생할 수 있는데..!

 

phantom read란

같은 트랜잭션 내에서 조회를 2번 할 시, 처음에 보이지 않았던 데이터가 다른 트랜잭션에서 삽입을 하여

처음에 보지 못한 데이터가 유령처럼 보이는 현상을 뜻한다.

 

repeatable read는 읽기 잠금을 사용해 해당 데이터들의 수정/삭제를 막기 때문에, 새로 들어온 데이터는 해당사항이 없기 때문에 발생할 수 있는 것이다.

 

MySQL의 읽기잠금 조회 시는?

mysql에서 락이 걸리는 조회 시엔,

대상 테이블의 컬럼에 index가 걸려있는지, 해당 index가 유니크한지에 따라 달라진다.

 

어쨋든 일부 조건에선 스캔한 인덱스 범위에 lock(gap lock, next-key lock)을 적용해서 다른 트랜잭션에 의해 해당 범위 내에 insert되는 것을 막는다.

 

어떤조건인지, 어떻게 막는지, 락에 대해 더 깊게 알고 싶다면 아래 블로그가 잘 써놨다..!

https://www.letmecompile.com/mysql-innodb-lock-deadlock/

 

근데 왜 타 트랜잭션에서 insert 된거지?

처음에 동기는 트랜잭션을 열고 다른 트랜잭션에서 인서트 후 커밋이 되었다고 했다.

 

왜 insert가 되었을까 확인해보니,

innoDB의 repeatable read에선 읽기 시에 잠금을 걸지 않는다고 한다.

이유는 락을 사용하지 않으면 다른 곳에서 막히지 않기 때문에 동시성이 높아지기 때문이다.

 

그래서 lock 대신 Consistent read를 사용한다!

 

Consistent read(Consistent nonlocking read)란?

일관된 읽기란 뜻으로, InnoDB의 다중 버전 관리(MVCC)를 사용하여 특정 시점의 데이터베이스 스냅샷을 이용하여 기존과 동일하게 결과를 보장해준다.

 

repeatable read에선, 트랜잭션 시작 후 첫번째 read 수행 시점의 데이터로 스냅샷이 생성된다.

그러므로 다른 트랜잭션에서 insert하더라도 스냅샷 기반으로 조회를 하기때문에 발견되지 않았던 것이다..!

 

스냅샷 이전에 커밋된 트랜잭션의 변경 사항을 확인하고 이후 또는 커밋되지 않은 트랜잭션의 변경 사항은 확인하지 않는다!

 

더 나가기..! Consistent read의 문제점은..?

Consistent read를 통해 다른 트랜잭션에서 데이터를 변경했더라도 동일한 결과를 보장한다고 했다.

트랜잭션은 트랜잭션 내에서 수행한 변경사항에 대한 결과가 항상 보여아한다.

 

이 과정에서, 여러 이상증상이 발생한다..ㅎㅎ;

 

1) update 케이스

: 다른 트랜잭션에서 insert한 데이터를 update 후 조회 시 해당 데이터가 보여진다.

또한 다른 트랜잭션에서 delete한 데이터를 update 시 update문이 적용되지 않은 채 조회된다.

(아래 케이스 중 update문 테스트 확인)

 

그 외에도 consistent read는 DDL 구문에서는 동작하지 않는다.

 

Consitent read 테스트들..

1) 스냅샷 테스트

더보기

트랜잭션 1 생성

 

트랜잭션 2 생성

인서트

커밋

 

트랜잭션 1에서 조회

 

인서트한 내용이 보임!

트랜잭션 1 생성

조회

 

트랜잭션 2 생성

인서트

커밋

 

트랜잭션 1 재조회

인서트한 내용이 안보임..!

 

이를 통해 스냅샷은 첫 조회 시 한다는 것을 알 수 있음.

 

2) delete 문, insert문 테스트

더보기

트랜잭션 1 생성

조회

 

트랜잭션 2 생성

인서트

커밋

 

트랜잭션 1 delete문 실행

트랜잭션 1 조회

인서트문은 보이지 않은 채 delete문은 실행된 데이터로 보임. (스냅샷 기반)

 

트랜잭션 1에서 동일 id insert문 실행 (중복 키 에러 발생. insert는 실제 db에 넣기 때문인듯)

 

트랜잭션 1 재조회 (중복 키 에러 뜨면서, 스냅샷 최신화 하나 테스트했으나 기존 스냅샷 기반으로 데이터 조회됨)

 

트랜잭션 1에서 다른 id insert문 실행 후 조회 ( 기존 스냅샷 기반)

 

3) update문 테스트

 

더보기

트랜잭션 1 생성

조회

 

트랜잭션 2 생성

인서트

커밋

 

트랜잭션 1에서 임의 데이터 업데이트 후 조회 ( 기존 스냅샷과 동일 데이터에 업데이트만 되어서 반영)

트랜잭션 1에서 인서트된 데이터 업데이트 후 조회( 업데이트도 성공하고, 해당 데이터도 표기된다..!)

 

트랜잭션 1생성

조회

 

트랜잭션 2 생성

인서트(id = 10)

업데이트 (id = 5)

커밋

 

트랜잭션 1 조회 ( 인서트, 업데이트 둘다 반영되지 않음)

 

즉, 본인의 현재 스냅샷 외의 데이터의 업데이트 시 phantom read가 발생함..!!

(repeatable read의 동일 결과 보장 깨짐)

 

트랜잭션 1 생성

조회

 

트랜잭션 2 생성

딜리트 (id = 5)

커밋

 

트랜잭션 1 업데이트 (id =5)

조회 ( 해당 업데이트 내역이 반영되지 않음)

 

db상 삭제 된 데이터를 업데이트하고 조회 시 해당 내역이 반영되지 않음!

(transaction 제약조건 위반)

 

요약 및 해결

mysql 기반의 innoDB에선 default로 repeatable read 수준의 트랜잭션 고립수준을 가져가고,

consistent read(락을 걸지 않는 조회)를 쓴다.

 

해당 consistent read는 스냅샷 기반으로 하기 때문에, phantom read가 거의 발생하지 않는다.

 

몇개의 예외 케이스로는,

타 트랜잭션에서 인서트한 것을 업데이트하면 조회 시 해당 내용이 보임.

타 트랜잭션에서 딜리트한 것을 업데이트하면 해당 업데이트 내역이 반영 안된 것으로 조회됨.

 

동기가 원했던 타 트랜잭션에서 업데이트한 내역을 보기 위해선,

 

1) 조회 트랜잭션을 늦게 연다 [이게 맞다고 생각듬]

2) 격리 수준을 낮춘다 ( 비추 )

 

참조

https://nesoy.github.io/articles/2019-05/Database-Transaction-isolation (트랜잭션 고립수준)

https://zzang9ha.tistory.com/381 (트랜잭션 고립수준)

https://devlog-wjdrbs96.tistory.com/424 (트랜잭션 전파 종류)

https://dev.mysql.com/doc/refman/8.0/en/innodb-consistent-read.html (Consistent read MySQL)

https://www.letmecompile.com/mysql-innodb-transaction-model/ (Mysql repeatable read 블로그 글)

댓글