본문 바로가기

MySQL

MySQL InnoDB 트랜잭션 개념과 Isolation level

트랜잭션 개념

하나의 논리적 작업 단위를 구성하는 일련의 연산

 

트랜잭션 ACID

원자성 ( Atomicity ) : all or nothing, 완료되지 않은 중간 상태를 데이터베이스에 반영해서는 안 된다. 즉, 트랜잭션의 모든 연산들이 정상적으로 수행 완료되거나 아니면 어떠한 연산도 수행되지 않은 상태를 보장해야 한다.

일관성 ( Consistency ) : 성공적으로 수행된 트랜잭션은 정당한 데이터들만을 데이터베이스에 반영해야 한다. 트랜잭션 수행이 보존해야 할 일관성은 기본 키, 외래 키 제약과 같은 명시적인 무결성 조건들 뿐만 아니라 두 계과 잔고의 합은 이체 전후가 같아야 한다는 사항과 같은 비명시적인 일고나성 조건들도 있다.

독립성 ( Isolation ) : 여러 트랜잭션이 동시에 수행되더라도 각각의 트랜잭션은 다른 트랜잭션의 수행에 영향을 받지 않고 독립적으로 수행되어야 한다.

DBMS는 병렬적 수행의 장점을 얻기 위해서 트랜잭션을 병렬적으로 수행하면서도 일렬(serial) 수행과 같은 결과를 보장할 수 있는 방식을 제공하고 있다.

지속성 ( Durability ) : 트랜잭션이 성공적으로 완료되어 커밋되고 나면, 항후 어떠한 장애가 발생되더라도 보존되어야 한다.

 

MySQL InnoDB의 데이터 기록 정책

MySQL 5.7 Reference Guide

 

InnoDB uses an optimistic mechanism for commits, so that changes can be written to the data files before the commit actually occurs. This technique makes the commit itself faster, with the tradeoff that more work is required in case of a rollback.

 

→ InnoDB에서는 최적화 매커니즘을 사용하고, commit되지 않은 데이터를 디스크에 기록할 수 있습니다. 이에 대한 트레이드 오프 때문에 rollback이 필요한 케이스가 발생할 수 있습니다.

 

즉, 트랜잭션이 commit 되기 이전에 에러가 발생한다면, atomicity ( 원자성 )과 consistency ( 일관성 )을 보장해주기 위해서 데이터를 롤백해야 합니다.

 

MySQL 에서는 데이터를 롤백하기 위해서 Undo Log를 이용합니다.

 

MySQL의 페이지 버퍼 정책

MySQL 5.7 Reference Guide

 

A memory or disk area used for temporary storage. Data is buffered in memory so that it can be written to disk efficiently, with a few large I/O operations rather than many small ones.

 

→ 데이터는 버퍼되어서 디스크에 효율적으로 쓰여질 수 있습니다.

 

During normal operation, committed data can be stored in the change buffer for a period of time before being written to the data files.

 

→ 커밋된 데이터들은 파일에 기록되기 전에 일정 시간동안 change buffer 내부에 저장될 수 있습니다.

 

즉, 트랜잭션이 commit 되었지만, 아직 데이터가 디스크에 저장되지 않은 상태에서 장애가 발생한다면 Durability ( 지속성 )을 보장해주기 위해서 데이터를 복구해야 합니다.

 

MySQL 에서는 데이터를 복구하기 위해서 Redo Log를 사용합니다.

 

트랜잭션 롤백이 이루어지는 과정

만약 프로그램이 동작하다가 특정 트랜잭션에서 에러가 생겨서 롤백해야 하는 일이 발생한다면, 로그를 역순으로 찾아가면서 Undo 연산이 필요한 지점을 찾아 Undo 연산을 진행합니다.

 

Undo 연산이 완료되면 이에 대한 로그를 적게 되고, 트랜잭션 시작 지점까지 도달하게 되면 롤백이 완료됩니다.

 

트랜잭션 복구가 이루어지는 과정

장애 발생 이후 데이터베이스를 재시작 복구해야 하는 경우 3 단계로 복구가 이루어 집니다.

  1. 마지막 체크 포인트부터, 최근 로그까지 탐색 하며 복구 시작 지점을 찾습니다.
  2. 복구 시작 시점부터, 장애 발생 시점 직전까지 Redo 로그를 이용하여 실행된 연산들을 Redo 합니다. 이 때, 실패한 트랜잭션들도 모두 Redo 합니다.
  3. 가장 최근 로그에서부터 실패한 트랜잭션들을 찾아 Undo 연산을 진행합니다.

 

MySQL의 Isolation level 정책

MySQL 5.7 Reference guide

 

Transaction isolation is one of the foundations of database processing. Isolation is the I in the acronym ACID; the isolation level is the setting that fine-tunes the balance between performance and reliability, consistency, and reproducibility of results when multiple transactions are making changes and performing queries at the same time.

 

→ 트랜잭션 isolation ( 독립성 )은 기본적인 데이터베이스 처리 방식입니다. 아중에서 I에 해당하는 Isolation 을 지키기 위해서는 모든 트랜잭션을 순차적으로 진행시키거나, lock을 이용하여 병렬적으로 수행되면서도 순차적으로 실행하는 것과 같은 결과를 보장해야 합니다.

 

하지만, isolation의 특성을 완벽하게 지키면 애플리케이션의 성능이 많이 떨어지기 때문에 DBMS에서는 reliability, consistency, reproducibility를 희생하여 performance를 올릴 수 있는 isolation level 옵션을 제공합니다.

 

isolation level에는 READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE 4 가지 단계가 있습니다.

 

이중에서 READ UNCOMMITTED의 경우 정합성에 많은 문제가 있기 때문에 거의 사용하지 않으며, SERIALIZABLE의 경우 동시성이 중요한 데이터베이스에서는 거의 사용하지 않습니다.

 

성능의 경우 SERIALIZABLE이 아니라면 크게 성능이 떨어지는 경우는 발생하지 않습니다.

 

READ UNCOMMITTED

read uncommitted isolation level 에서는 각 트랜잭션이 동시에 실행되고 있을 때, 커밋되거나 롤백되지 않은 데이터들도, 다른 트랜잭션에 보여지게 됩니다.

 

트랜잭션 1

 

emp_no 500000 insert 후 5초 기다리다 rollback

 

start transaction; 

insert into employees(emp_no, birth_date, first_name, last_name, gender, hire_date) value(500000, '1953-09-02', 'Georgi', 'Facello', 'M', '1986-06-26'); 

do sleep(5); 

rollback;

 

 

트랜잭션 2

 

emp_no 500000 select

 

start transaction; 

select * from employees where emp_no = 500000; 

commit;

 

 

테스트 시나리오

  • 트랜잭션 1에서 insert sql문이 실행된 이후 5초 동안 sleep 상태가 될 때 트랜잭션 2를 실행합니다.

 

테스트 결과

 

 

트랜잭션1에서 emp_no가 500000인 row는 rollback 되어 사라졌지만, READ UNCOMMITTED에서는

트랜잭션이 commit 되거나, rollback 되지 않은 데이터도 읽어오기 때문에 트랜잭션 2에서는 조회가 된 모습을 볼 수 있습니다.

 

READ COMMITTED

Read Committed isolation level에서는 Commit 된 데이터만 다른 데이터베이스에서 조회할 수 있습니다.

 

트랜잭션 1

 

emp_no가 500000인 row의 fisrt_name을 Toto로 변경한 이후에 5초 기다렸다 커밋

 

start transaction; 

update employees set first_name = 'Toto' where emp_no = 500000; 

do sleep(5); 

commit;

 

 

트랜잭션 2

 

emp_no가 500000인 row select

 

start transaction; 

select from employees where emp_no = 500000; 

commit;

 

 

테스트 시나리오

  • 트랜잭션 1에서 update를 한 이후, 5초간 기다릴 때, 트랜잭션 2 실행

 

테스트 결과

 

 

트랜잭션 1에서 ‘Toto’로 row를 업데이트 했지만, 트랜잭션 2에서는 이전 fisrt_name인 Lara를 select 했습니다.

즉, 커밋되지 않은 ‘Toto’ update 건은 트랜잭션 2에서 조회되지 않은것입니다.

 

그렇다면 Read Committed에서는 어떻게 이전 데이터를 가져올 수 있었을까요?

 

MySQL InnoDB에서는 트랜잭션 내부에서 INSERT, UPDATE, DELETE 시 변경 이전의 사항을 추적, Rollback 복구를 위해 Undo 영역에 Log를 기록합니다.

 

Read Committed에서는 Undo 영역에 기록된 변경되지 않은 데이터를 가져와서 보여주는 있고,

위 예제에서 트랜잭션2 에서는 Undo 영역에 있는 변경되지 않은 데이터인 Lara를 보여주는 것입니다.

 

Non Repeatable Read 정합성 문제

 

Read Committed 격리 수준에서는 Repeatable Read가 불가능하다는 문제가 있습니다.

emp_no가 500000이고 first_name이 ‘Toto’인 데이터에 대해서

 

트랜잭션 1

 

5초의 간격을 두고, first_name이 ‘Toto’인 row select

 

start transaction; 

select * from employees where first_name = 'Toto'; 

do sleep(5); 

select * from employees where first_name = 'Toto'; 

commit;

 

 

트랜잭션 2

 

emp_no가 500000인 row를 ‘Lara’로 변경

 

start transaction; 

update employees set first_name = 'Lara' where emp_no = 500000; 

commit;

 

 

테스트 시나리오

  • 트랜잭션 1을 실행하고 첫 번째 select 결과가 나온뒤, 트랜잭션 2를 실행합니다.

 

테스트 결과

 

첫 번째 select

 

 

두 번째 select

 

 

첫 번째 select에서는 emp_no가 500000인 데이터를 읽었지만 두 번째 select 에서는 update의 commit을 반영하여 아무것도 select 되지 않았습니다.

 

데이터가 중요하지 않은 경우 이 부분은 문제가 되지 않을 수 있지만, 돈과 같이 중요한 데이터를 다루는 곳에서는 큰 문제가 될 수 있습니다.

 

Repeatable Read

 

Repeatable Read에서는 아까와 같은 문제가 발생하지 않습니다.

 

아까 한 테스트를 Repeatable Read에서 다시 실행해보면 아래와 같은 결과가 나오게 됩니다.

 

첫 번째 select

 

 

두 번째 select

 

 

어떻게 이런 것이 가능했을까요?

 

Repeatable Read에서는 Read Committed와 동일하게 Undo 영역을 참고하지만, 좀 더 이전의 데이터를 참고합니다.

 

Read Committed에서는 Undo 영역에 있는 최근 데이터를 확인하지만, Repeatable Read에서는 select 을 실행시킨 트랜잭션보다 더 이전의 트랜잭션 에서 Undo 영역에 write한 데이터만을 참고합니다.

 

그렇기 때문에, 매번 같은 데이터를 읽어드리는 것을 보장해주게 됩니다.

 

하지만 Repeatable Read에서도 부정합이 발생할 수 있습니다.

 

Select … For Update 같이 락을 사용한 읽기를 사용하게 되면 Undo 영역에는 잠금을 걸 수 없기 때문에 Undo 영역에서 데이터를 가져오는게 아니라 현재 데이터를 가져오게 됩니다.

 

즉, 데이터가 보였다, 안보였다 하는 Phantom Read 문제가 발생할 수 있게 됩니다.

 

Phantom Read 문제를 해결하기 위해서는 일반적으로 Serializable 을 사용해야 합니다.

 

그런데 MySQL InnoDB의 경우 Repeatable Read 에서도 Phantom Read 문제가 발생하지 않습니다.

Serializable

가장 엄격한 격리 수준으로 그만큼 동시 처리 성능도 다른 격리 수준보다 떨어집니다.

Serializable에서는 읽기 작업도 잠금을 획득해야 하기 때문에 앞서 언급했던 모든 부정합 문제는 사라집니다.

 

출처

 

https://d2.naver.com/helloworld/407507 ( DBMS는 어떻게 트랜잭션을 관리할까? )

https://dev.mysql.com/doc/refman/5.7/en/introduction.html ( MySQL 5.7 reference guide )

Real MySQL ( 이성욱 )