- 트랜잭션이 데이터를 읽기 직전에 그 데이터에 읽기 락을 건다.
읽기 락은 여러 개가 중복될 수 있다.
그래서 동시에 여러 트랜잭션이 같은 데이터를 읽는 것은 가능하다. - 읽기 락과 쓰기 락은 중복될 수 없다.
어떤 트랜잭션이 데이터를 읽는 중이라서 읽기 락이 걸려있는 데이터를
다른 트랜잭션이 수정하는 것은 불가능하다.
데이터 읽기가 끝나고 읽기 락이 풀리면, 그때 쓰기 락을 걸고 수정하게 된다.
- 트랜잭션이 데이터를 읽을 때, 먼저 그 데이터에 읽기 락이 자동으로 걸려야 한다.
그런데 읽기 락이 언제나 자동으로 걸리는 것은 아니다.읽기 락의 여부는 Transaction Isolation Level 설정에 따라 다르다.
읽기 락이 언제까지 유효한지도 Transaction Isolation Level 설정에 따라 다르다.
- 트랜잭션이 데이터를 쓰기 직전에 그 데이터에 쓰기 락을 건다.
쓰기 락은 여러 개 중복될 수 없다.
그래서 동시에 여러 트랜잭션이 같은 데이터를 수정하는 것은 불가능하다. - 읽기 락과 쓰기 락은 중복될 수 없다.
어떤 트랜잭션이 데이터를 수정하는 중이라서 쓰기 락이 걸려 있는 데이터를
다른 트랜잭션이 읽는 것은 불가능하다.
데이터 수정이 끝나고 쓰기 락이 풀리면, 그때 읽기 락을 걸고 읽게 된다.
- 트랜잭션이 데이터를 수정할 때, 그 데이터에 자동으로 쓰기 락이 걸린다.
데이터 수정 전에 쓰기 락이 걸리는 것은 언제나 자동으로 일어난다.
언제나 트랜잭션이 종료될 때 쓰기 락은 풀린다. (unlock) - 즉, 쓰기 락은 데이터를 수정하기 직전에 언제나 자동으로 걸리고,
트랜잭션이 종료될 때 풀린다.
- Transaction Isolation Level 설정에 따라 트랜잭션이 데이터를 읽기 전에 읽기 락 여부와 읽기 락 유효 범위가 결정된다.
읽기 락을 많이 걸고 오래 유지할수록 데이터의 안정성은 좋아지지만 성능은 나빠진다.
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
- 위 목록에서 아래의 명령일수록 읽기 락을 더 많이 건다.
Transaction Isolation Level 을 설정하지 않으면 기본값은 다음과 같다.
Oracle - READ COMMITTED
SQL Server - READ COMMITTED
MySQL - REPEATABLE READ
읽기 락을 전혀 하지 않는다. 그래서 가장 빠르다.
Dirty Reads 문제 발생
- 읽기 락을 전혀 하지 않기 때문에 다른 트랜잭션이 수정하고 있어서 쓰기 락이 걸려 있는 데이터도 읽을 수 있다.
그래서 게시글 본문이 반쯤 저장된 상태에서 그 게시글을 읽는 것이 가능하다.
이렇게 완전하지 않은 데이터가 읽혀질 수 있는 문제를 Dirty Reads 문제라고 한다. - 읽기 락을 하지 않기 때문에 어떤 트랜잭션이 읽고 있는 데이터를 다른 트랜잭션이 쓰기 락을 걸고 수정, 삭제할 수도 있다.
데이터를 읽기 직전에 언제나 읽기 락을 건다.
Non-Repeatable Reads 문제 발생
- 예시로 계좌이체를 보자.
(1) 잔고가 충분한지 파악하기 위해 계좌의 금액을 읽어 잔고가 충분하지 않다면 종료
(2) 잔고가 충분하다면 계좌이체 실행
위와 같은 순서로 구현했을 때
(1) 에서 계좌의 금액을 읽을 때, 읽기 락이 걸리지만, (1)의 SQL 문 실행이 끝나자마자 읽기 락은 풀린다.
이렇게 읽기 락이 풀리고 아직 (2)를 실행하기 전,
다른 트랜잭션이 바로 그 계좌에 쓰기 락을 걸고 계좌의 금액을 수정하는 것이 가능하다.(인출)
사이에 끼어든 트랜잭션이 쓰기 락을 푼 다음에야 (2)가 시작될 수 있는데
아까 (1)을 실행할 때는 잔고가 충분했지만, 다른 트랜잭션이 인출했기 때문에 이제는 잔고가 충분하지 않을 수 있다.
그러므로 (2)에서 에러가 발생할 수 있다.
즉, (1)과 (2) 사이에 다른 트랜잭션이 끼어들어 데이터를 수정하는 것이 가능하기 때문에
(1)에서 읽은 잔고와 (2)에서 읽은 잔고가 다를 수 있다.
이렇게 하나의 트랜잭션에서 어떤 데이터를 처음 읽을 때와 나중에 읽을 때 값이 달라 문제가 되는 상황을
Non-Repeatable Reads 문제라고 한다.
데이터를 읽기 직전에 읽기 락을 건다.
그리고 트랜잭션이 끝날 때까지 읽기 락을 유지해서 다른 트랜잭션이 사이에 끼어들지 못하도록 한다.
- Non-Repeatable Reads 문제 해결
위의 계좌이체 절차를 다시 생각해보자.
(1)에서 계좌의 금액을 읽을 때 읽기 락이 걸리고, 계좌이체 트랜잭션이 종료될 때까지 읽기 락이 유지된다.
읽기 락이 유지되고 있으므로 다른 트랜잭션이 그 데이터를 수정하기 위해 쓰기 락을 걸 수 없다.
따라서 트랜잭션이 종료될 때까지 그 계좌의 데이터는 다른 트랜잭션이 수정할 수 없다.
(2)에서 읽은 잔고는 (1)에서 읽은 값과 언제나 동일하다.
계좌이체를 진행하려면 그 계좌의 읽기 락을 쓰기 락으로 승격해야 한다.
읽기 락이 걸려 있는 데이터에 다른 트랜잭션이 쓰기 락을 걸 수는 없지만,
읽기 락은 건 바로 그 트랜잭션은 자신이 걸었던 읽기 락을 쓰기 락으로 변경할 수 있다.
(2)에서 계좌이체 트랜잭션은 계좌의 읽기 락을 쓰기 락으로 변경한 다음 진행된다.
Phantom Reads 문제 발생
- 예시로 수강신청을 보자.
(1) 강좌의 수강 레코드 수를 조회하여 최대 수강 인원 수보다 크거나 같다면 종료
(2) 그렇지 않다면 수강 레코드 삽입(INSERT)
위와 같은 순서로 구현했을 때
(1)에서 읽은 수강 레코드들에 읽기 락이 걸리고 트랜잭션이 끝날 때까지 유지된다.
그 트랜잭션이 끝날 때까지 수강 레코드들은 수정될 수 없도록 보호된다.
하지만, 새 수강 레코드가 삽입되는 것은 가능하다.
그래서 (1)과 (2) 사이에 다른 트랜잭션이 그 강좌에 새 레코드를 삽입할 수 있다.
(1)에서 강좌의 수강 레코드 수를 조회할 때는 최대 수강 인원 수보다 적었는데
막상 (2)를 실행할 때는 최대 수강 인원 수와 수강 레코드 수가 같을 수 있다.
(1)과 (2) 사이에 다른 트랜잭션이 수강 레코드를 등록할 수 있기 때문이다.
(1)과 (2)에서 조회한 수강 레코드 수가 같으려면 다른 트랜잭션이 그 강좌에 수강 레코드를 등록하지 못하게 해야 한다.
-
테이블에 읽기 락을 걸거나, 테이블 인덱스에 읽기 락을 걸거나, WHERE 절 조건식으로 읽기 락을 걸기도 한다.
테이블에 읽기 락이 걸리면, 그 테이블에 대한 모든 수정(CUD)는 막힌다.
테이블 인덱스에 읽기 락이 걸리면, 그 인덱스에 변화를 초래하다 수정(CUD)는 막힌다.
WHERE 절 조건식으로 읽기 락이 걸리면, WHERE 절 조건식의 true/false 값이 변할 만한 수정(CUD)는 막힌다. -
Phantom Reads 문제 해결
수강신청 절차를 다시 생각해보자.
강좌의 수강 레코드 수 조회 SQL 문은 다음과 같은 형태일 것이다.
SELECT COUNT(*) FROM 수강 WHERE lectureId = #{lectureId}
(1)에서 읽은 강좌 레코드 수를 조회하는 WHERE 절 조건식에 읽기 락이 걸린다.
WHERE 조건식의 값이 어떤 명령의 실행 전과 후에 달라진다면, 읽기 락에 의해 그 명령의 실행은 막힌다.
WHERE 조건식이 true 인 레코드를 DELETE 하는 것도 막힌다.
WHERE 조건식이 true 인 레코드를 INSERT 하는 것도 막힌다.
따라서 (1)과 (2) 사이에 다른 레코드가 끼어들어 그 강좌에 새 수강 레코드를 삽입할 수 없다. -
Serializable 단계는 모든 읽기 문제가 해결된 단계다.