최근에 데이터 중심 어플리케이션을 읽고 있는데 트랜잭션 내용을 CS 스터디에서 발표하게 되어 정리해보았다.
주요 포인트는 아래와 같다
원자성 (A) : 핵심은 중간에 에러 발생시 Abort 될 수 있는 기능
일관성 (C) : 실제로는 DB 책임이 아닌 어플리케이션 책임으로 보는 것이 타당하다.
격리성 (I) : 한 트랜잭션이 여러 번 쓴다면 다른 트랜잭션은 그 내용을 전부 볼 수 있든지 아무것도 볼수 없든지 둘 중 하나여야 한다는 뜻.
지속성 (D) : 트랜잭션이 성공적으로 커밋되면 하드웨어 결함 등이 발생해도 데이터는 소실되지 않아야 한다.
: 더티 읽기, 쓰기 방지
: 스냅숏 격리와 사실상 같음(?) → SQL 표준에 스냅숏 격리 개념이 없기 때문. 따라서 데이터베이스마다 의미가 가지가지임
핵심 아이디어 : 읽는 쪽은 쓰는쪽 차단 X, 쓰는 쪽은 읽는 쪽 차단 X ⇒ MVCC
만약 READ COMMITTED 만 구현한다면 객체마다 2개의 버전 (커밋 버전, 수정되었으나 커밋되지 않은 버전) 만 필요하다
그러나 스냅숏 격리를 지원하는 경우 읽기 격리를 위해 질의마다 독립정인 스냅숏을 사용하고, 전체 트랜잭션에 대해 동일한 스냅숏 사용
트랜잭션 ID가 증가함으로, 현재 트랜잭션이 시작한 후에 시작한 트랜잭션이 쓴 데이터는 무시됨, abort 된 데이터도 물론 무시됨
즉, 스냅숏은 REPETABLE READ나 SERIALIZABLE에 사용된다고 생각하면 된다. (근데 Oracle은 READ COMMITTED 인데 MVCC 사용한다고 함. 다 그런 것은 아닌듯)
스냅숏 생성 타이밍 구현 차이
더티 쓰기 → 두 트랜잭션이 같은 객체 갱신
갱신 손실 → 두 트랜잭션이 같은 객체 갱신
UPDATE a_table SET content = ‘new’ WHERE id = 1234 AND content = ‘old’ ) → 그러나 만약 DB 구현상 WHERE 절이 오래된 스냅샷을 읽는다면 문제 발생쓰기 스큐 → 두 트랜잭션이 여러 객체들을 조회하는데, 일부 같은 객체들을 읽고, 다른 객체를 갱신
원자적 연산 무의미, 갱신손실 감지 불가
락 (비관적, 낙천적락) 등으로 명시적으로 row를 잠그거나 진짜 직렬성 격리 수준으로 해결해야
예시
팬텀 : 트랜잭션이 실행한 쓰기가 다른 트랜잭션의 검색조건을 바꾸는 경우. 쓰기 스큐를 유발
보통의 트랜잭션 순서
여기서 만약 CUD가 SELECT 조건 판단에 영향을 준다면? 만약 INSERT로 인해 락을 걸 수도 없다면??
MySQL은 범위 질의에서 갭락으로 방지함
비슷하게 회의실 문제를 예약할 수 있는 시간을 특정 간격으로 나누어 미리 회의실을 만들고 SELECT에 해당하는 회의실을 Lock을 걸 수 있다 (SELECT FOR UPDATE)
문제 해결 방안
: 여러 트랜잭션이 병렬로 실행되더라도 최종 결과는 하나씩 직렬로 실행될 때와 같도록 보장. 완화된 격리조건과 반대!
공유락, 배타락!
스냅숏 격리는 읽는쪽과 쓰는쪽은 서로를 막지 않지만, 2PL은 쓰기를 원하는 객체에 대해서는 읽기, 쓰기 모두 불가능하게 하고, 읽기를 원하는 객체에 대해서는 쓰기를 불가능하게 한다.
교착상태 발생할 확률 높음. 성능 이슈 주의
2.1 서술 잠금 (Predicate Lock, MySQL에서의 Gap Lock)
특정 범위에 대해서 SELECT하게 되면 미래에 추가될 수 있는 팬텀 객체에 대해서도 서술잠금이 적용됨
실제로는 index-range Lock, next-key Lock으로 구현
범위 잠금을 사용하는데에 있어, 검색조건에 index가 걸어두는 것이 매우 좋다. ⇒ 오버헤드가 매우 낮아짐
index가 없으면 테이블 전체 공유잠금을 걸게 됨 (성능 나쁘나 안전)
@Transactional(readOnly = true)가 일반 @Transactional보다 성능이 더 좋다전 주에 정리했던 내용에 추가적으로 InnoDB에서 Phantom Read가 발생하는 보다 더 구체적인 예시를 찾아서 공유하고자 한다.
다른 트랜잭션에서 데이터를 추가한 후에 내 트랜잭션에서 “UPDATE”하는 경우이다.
처음에 없던 데이터가 UPDATE 되기도하고, 이후에 SELECT하면 보이게 된다.
지금부터는 이 블로그 의 내용을 발췌한 것임을 밝힌다!!!!!! 더 자세한 내용은 꼭 이 블로그를 읽어보길 바란다.

오른쪽 트랜잭션에서 첫 SELECT 구문에서는 아무런 데이터가 없었으나, 이후 다른 트랜잭션에서 데이터를 INSERT 하였고, 그 INSERT한 데이터에 대해 UPDATE를 할 수 있게 된다..!

심지어 UPDATE 이후 SELECT 구문을 다시 날리면 UPDATE된 glen 데이터를 볼 수 있다.
어떻게 된 것일까?
블로그에 너무 잘 설명되어있어 부끄럽지만 그대로 가지고 왔다. 같이 이해해보자.
우선 InnoDB에서 Phantom Read를 막는 방법은 언두 로그, 트랜잭션 번호를 사용하여 Phantom Read를 방지한다.
A 트랜잭션을 시작하고, 데이터를 조회한다. A 트랜잭션 번호는 3이다.
B 트랜잭션을 시작하고, 데이터를 추가하고 커밋한다. B 트랜잭션 번호는 4이다.
그리고 다시 A 트랜잭션이 데이터를 조회할 때 언두 로그를 조회하는데, 언두 로그에 자신의 트랜잭션 번호보다 큰 트랜잭션 번호는 조회하지 않는다. (B 트랜잭션이 추가한 데이터는 트랜잭션 번호가 4번)
따라서 Phantom Read가 발생하지 않는다.
하지만 UPDATE 쿼리를 실행했을 때, Phantom Read가 발생하는 이유는 다음과 같다.
UPDATE 쿼리는 쓰기 락이 걸린다.UPDATE 쿼리가 반영되며, 언두 로그에 자신의 트랜잭션 번호가 갱신된 데이터가 생긴다.SELECT 쿼리를 실행하면 언두 로그에 있는 데이터를 조회한다.Phantom Read가 발생한다.