들어가며
트랜잭션을 말할 때 ACID 얘기를 하지 않을수가 없죠.
개인적으로 ACID의 다른 어떤 요소보다도 가장 중요한게 Isolation
이 아닐까 하는데요.Isolation
을 이해해야 비로소 트랜잭션에 대해 말 할 수 있다고 생각합니다.
Isolation
을 처음 이해하고 설레였던(!) 기억이 있네요.
이 문서를 통해 MySQL InnoDB 엔진
과 함께 격리 수준을 달리 하며, 각 수준에서 볼 수 있는 현상을 확인하는 hands-on을 진행합니다.
Transaction Isolation Level
트랜잭션 개념이 잘 정리된 자료는 어렵지 않게 찾아볼 수 있는데요.
개념과 특징에 대해 나열하는 것이 이 문서의 목적은 아니기에, 간단히만 다루고 넘어가겠습니다.
Transaction Isolation Level
- 트랜잭션의 격리 수준
- 트랜잭션이 동시에 처리될 때, 특정 트랜잭션이 다른 트랜잭션의 영향을 받는 수준을 결정
READ UNCOMMITED
READ COMMITED (DIRTY READ)
REPEATABLE READ
SERIALIZABLE
READ UNCOMMITED
에서SERIALIZABLE
로 갈 수록 높은 수준의 격리- 데이터베이스 격리 수준을 이야기할 때, 부정합 현상이 나타나는 몇가지 문제가 있음
- 일반적으로 3개 정도 다룸
DIRTY READ
,NON_REPEATABLE READ
,PHANTOM READ
Hands-on
트랜잭션 격리 수준을 바꿔 가며, 각 격리 수준에서 볼 수 있는 현상을 확인합니다.
테스트 진행을 위해 테이블과, 몇 개의 레코드를 생성합니다.
CREATE TABLE ENGINEERS (
id INT AUTO_INCREMENT,
firstName VARCHAR(50),
lastName VARCHAR(50),
email VARCHAR(100),
specialty VARCHAR(100),
yearsExperience INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
INSERT INTO ENGINEERS (firstName, lastName, email, specialty, yearsExperience)
VALUES ('jude', 'jahng', 'jude.jahng@kakaoeneterprise.com', 'Database Engineering', 0),
('jay', 'jahng', 'wkdwoos@gmail.com', 'Software Engineer', 2),
('jaeyoung', 'jahng', 'wkdcloud@gmail.com', 'Cloud Engineering', 0);
쿼리를 통해 생성한 테스트 테이블은 다음과 같은 모습입니다.
READ UNCOMMITED
첫 번째 격리 수준은 READ UNCOMMITED
입니다.
다른 트랜잭션에서 커밋되지 않은 데이터도 조회할 수 있는 가장 낮은 격리 수준인데요.
높은 동시성을 보장하지만, 반면 가장 낮은 격리 수준을 제공합니다.
핸즈온을 통해 직접 확인해보겠습니다.
MySQL 프로세스를 올리고, 두 개 세션에 접근합니다.
화면에서 위의 세션을 세션 1, 아래 세션을 세션 2 라고 하겠습니다.
mysql> SET @@session.transaction_isolation = 'READ-UNCOMMITTED';
Query OK, 0 rows affected (0.00 sec)
세션 2
에서 조회를 하겠습니다. 세션 2
의 트랜잭션 격리 수준을 READ-UNCOMMITED
로 설정합니다.
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> UPDATE ENGINEERS
-> SET yearsExperience = 1
-> WHERE id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
세션 1
에서 트랜잭션을 시작하고, ENGINEERS
테이블의 id = 1
레코드의 yearsExperiences
값을 업데이트합니다.
세션 2
에서 조회했을 때, 해당 레코드가 변경된 것을 확인할 수 있습니다.
이때 트랜잭션은 아직 UNCOMMITED
상태입니다.
세션 1
에서 트랜잭션을 COMMIT
하지 않고 ABORT(ROLLBACK)
하면,
업데이트한 레코드가 다시 이전으로 돌아간 것을 확인할 수 있습니다.
세션 2
에서 이를 확인 하면,
여기서도 업데이트한 레코드의 yearsExperience 컬럼의 값이 다시 0으로 조회되는 것을 확인할 수 있습니다.
READ UNCOMMITED
격리 수준에서 볼 수 있는 DIRTY READ
현상이 바로 이것입니다.READ UNCOMMITED
, 말 그대로 커밋되지 않은 트랜잭션에 대해 조회할 수 있는 수준의 격리가 되겠습니다.
READ COMMITED
커밋된 결과에 대해서만 조회할 수 있는 격리 수준으로,
Oracle DBMS의 default이며, OLTP에서 가장 많이 사용되는 격리 수준입니다.
mysql> SET @@session.transaction_isolation = 'READ-COMMITTED';
Query OK, 0 rows affected (0.00 sec)
조회 역할의 세션 2
의 트랜잭션 격리 수준을 READ-COMMITED
로 설정합니다.
start TRANSACTION;
UPDATE ENGINEERS SET specialty = 'DevOps Engineer' WHERE id = 2;
SELECT e.id, e.firstName, e.lastName, e.specialty FROM ENGINEERS e WHERE e.id = 2;
세션 1
에서 트랜잭션을 시작하고, id = 2
인 레코드를 변경합니다. 아직 트랜잭션은 UNCOMMITTED
입니다.
세션 2
에서 해당 테이블을 조회했지만, 세션은 격리 수준을 READ-COMMITTED
로 설정했기 때문에 UNCOMMITTED
인 변경 사항을 조회할 수 없습니다.
이제 이 상황에서 세션 1
에서 진행중인 트랜잭션을 COMMIT
하면,
COMMIT
된 변경사항을 세션 2
에서 확인할 수 있습니다.
NON REATABLE READ
앞서 READ COMMITED
수준의 격리는 NON-REPEATABLE READ
현상이 나타날 수 있음을 이야기 했는데요,
이번에는 NON-REPEATABLE READ
를 확인해 보겠습니다.
이번에도 서로 다른 두 개의 트랜잭션을 시작합니다.
현재 병렬되는 두 개 트랜잭션이 보고 있는 테이블의 상태는 다음과 같습니다.
이번에도 위의 세션을 세션 1
아래의 세션을 세션 2
라고 하겠습니다.
세션 2
에서 id = 2
인 레코드의 현재 specialty의 값은 ‘DevOps Engineer’ 입니다.
UPDATE ENGINEERS SET specialty = 'Software Engineer' WHERE id = 2;
SELECT e.id, e.firstName, e.lastName, e.specialty FROM ENGINEERS e WHERE e.id = 2;
COMMIT;
세션 1
에서 id = 2인 ROW의 specialty 컬럼 값을 ‘Software Engineer’ 로 변경하고, COMMIT
합니다.
그 다음, 세션 2
에서 id = 2
인 레코드를 조회하면,
SELECT e.id, e.firstName, e.lastName, e.specialty FROM ENGINEERS e WHERE e.id = 2;
specialty의 값이 ‘Software Engineer'로 변경되어 있음을 확인할 수 있습니다.
이처럼 하나의 트랜잭션에서 동일한 테이블에 대해 반복하여(REPEAT) 조회 했을 때, 이전 조회와 결과가 서로 다른 상황,
반복 가능하지 않은 조회 NON-REPEATABLE READ
가 나타난 것을 확인할 수 있습니다.
REPEATABLE READ
REPEATABLE READ
는 MySQL InnoDB
스토리지 엔진이 default로 사용하고 있는 격리 수준입니다.
binlog가 있는 MySQL 서버
에서는 최소 REPEATABLE READ
격리 수준 이상을 사용해야 하는데요,
REPEATABLE READ
는 말 그대로 반복 가능한 읽기를 보장하여, NON REPEATABLE READ
현상이 발생하지 않습니다.
InnoDB 스토리지 엔진
은 트랜잭션이 ROLLBACK 될 가능성에 대비해 변경 전 레코드를 언두 로그 형태로 언두 테이블 스페이스(이하 언두 영역)에 백업해두고, 실제 레코드를 변경하여 MVCC를 보장합니다.
당연히 이 undo tablespace
가 무한하진 않구요. 이 또한 엔지니어, 운영자의 관리 영역이 되겠습니다.
REPEATABLE READ
는 언두 영역에 백업된 이전 데이터를 이용하여 MVCC를 제공하고, 동일 트랜잭션 내에서는 동일한 결과를 보여줄 수 있도록 보장합니다.
이제 REPETABLE READ
핸즈온을 진행해 보겠습니다.
mysql> SET @@session.transaction_isolation = 'REPEATABLE-READ';
Query OK, 0 rows affected (0.00 sec)
조회 용도 세션 2
의 격리 수준을 REPEATABLE-READ
로 설정합니다.
이번에도 역시 서로 다른 두 개의 트랜잭션을 진행합니다.
현재 두 트랜잭션이 보고 있는 테이블의 상태는 다음과 같습니다.
이번에도 세션 1
, 세션 2
입니다.
세션 2
에서 id = 2
인 레코드의 현재 specialty의 값은 ‘Software Engineer’ 입니다.
사실 핸즈온 도중 오타를(!) 냈는데요
이 값을 'Software Engineering'
(!) 으로 업데이트 하겠습니다.
UPDATE ENGINEERS SET specialty = 'Software Engineering' WHERE id = 2;
COMMIT;
SELECT e.id, e.firstName, e.lastName, e.specialty FROM ENGINEERS e WHERE e.id = 2;
세션 1
에서 트랜잭션을 시작하고, 값을 변경합니다. 그리고 커밋을 찍습니다.
세션 1의 테이블 조회 결과는 당연히
레코드가 업데이트 된 것을 확인할 수 있네요.
그렇다면 세션 2에서는 어떨까요?
id = 2
인 레코드의 specialty 값이 이전 조회와 동일한 값(Software Engineer)으로 조회되었습니다.
REPEATABLE READ
가 보장된 것을 확인할 수 있네요.
지금까지 이야기를 종합해보면,
MySQL InnoDB 스토리지 엔진
이 채택한 REPEATABLE READ
는 DIRTY READ
, NON-REPEATABLE READ
가 발생하지 않는, 언뜻 완벽한 격리 수준인 것 처럼 보입니다.
그렇다면 세션 2가 지금 진행중인 트랜잭션이 종료된 이후에는 어떨까요?
트랜잭션이 종료되는 데는 두 가지 경우가 있습니다.
트랜잭션 COMMIT
트랜잭션 ABORT(ROLLBACK)
ABORT(ROLLBACK)
의 경우에는 트랜잭션이 테이블에 영향을 미치지 않기 때문에(정확히 말하자면, 변경사항이 없음),
우리가 주목해야 할 케이스는 아닐 것 같습니다.
그러나 테이블 변경사항이 적용되는 COMMIT
은 이야기가 다르지 않을까요.
다시 화면으로 돌아가 보겠습니다.
이 상황에서 세션 2
에서 id = 2
인 레코드의 ‘firstName’ 컬럼의 값을 변경하고, 조회해보겠습니다.
어떤 결과를 가져올까요.
UPDATE ENGINEERS SET firstName = 'GERRARD' WHERE id = 2;
SELECT e.id, e.firstName, e.lastName, e.specialty FROM ENGINEERS e WHERE e.id = 2;
우리는 세션 2
가 현재 진행중인 트랜잭션에서, id = 2
인 레코드에 대해 firstName 컬럼의 값만 변경하고(‘jay’ -> ‘GERRARD’) , specialty 컬럼의 값은 변경하지 않았습니다.
그러나 specialty의 값이 트랜잭션 전과 후가 다르게 조회되었네요.(‘Software Engineer’ -> ‘Software Eingeering’)
이러면 안되는거 아닐까요? 왜 그럴까요?
다른 트랜잭션에서 수행한 변경 작업에 의해,
'진행중인 동일한 트랜잭션 내에서' 같은 쿼리에 대해 레코드가 보이거나, 안보이거나, 또는 값이 달라지는 이상 조회 현상,
PHANTOM READ
가 발생한 것입니다.
세션 2
의 트랜잭션에서
UPDATE ENGINEERS SET firstName = 'GERRARD' WHERE id = 2;
쿼리는 ENGINEERS 테이블의 id = 2
레코드의 LOCK을 가져가지만, 언두 레코드에는 LOCK을 걸 수 없습니다.
때문에 세션 2
는 언두 영역의 변경 전 데이터를 가져오는 것이 아니라, 현재(latest) 언두 레코드의 값을 가져오게 되는 것입니다.
SERIALIZABLE
SERIALIZABLE
은 크게 설명할 것이 없습니다. 모든 작업이 잠금, 락 Lock
을 가져가며, 순차적으로 진행하는데요.
한 트랜잭션에서 읽고 쓰는 레코드를 다른 트랜잭션에서는 절대 접근할 수 없습니다.
읽기 작업도 공유 잠금(읽기 잠금)을 획득해야 하며, 가장 단순하면서도 엄격한 격리 수준이 되겠습니다.
그러나 성능이 매우 좋지 않아 거의 사용하지 않습니다.
결론
MySQL InnoDB 엔진
과 함께 격리 수준을 달리 하며, 각 격리 수준에서 볼 수 있는 현상을 핸즈온을 통해 확인했습니다.