[Database]Transaction과 isolation level

2021. 4. 11. 02:56 Database/mysql

Transaction이란?

Transaction이란 데이터베이스에서 실행하는 작업의 단위를 의미합니다.

 

이번 글에서는 송금하는 과정을 예제로 전체적인 개념들을 설명하고자 합니다.

또한 테스트를 위해 MySQL(InnoDB)을 활용하였습니다.

 

예제는 다음과 같습니다.

A라는 사람이 B라는 사람에게 송금을 하면,

아래와 같은 과정으로 이루어 진다고 가정하겠습니다.

  1. A 계좌에서 출금
  2. B 계좌에 위에서 차감된 금액만큼 입금

송금은 2 단계로 나누어지지만 크게는 하나의 transaction(작업)이라고 할 수 있습니다.

 

하지만 데이터베이스 관련된 모든 작업을 transaction이라고 하지는 않습니다.

Transaction은 다음과 같은 특징(ACID)을 가져야합니다.

Atomic

Atomic이란 '원자의'란 의미로 이는 원자처럼 '더이상 쪼갤 수 없는' 특징을 이야기할 때 사용합니다.

 

즉, Transaction은 최소 작업 단위로 더이상 나눌 수 없습니다.

 

위의 예시에서 송금 transaction은 출금(A 계좌로 부터)과 입금(B 계좌로)이라는 두 가지 작업을 수행하고 있습니다.

하지만 이는 하나의 transaction으로 묶여있기 때문에 위의 두 작업은 나눠질 수 없습니다.

다시 말해, 출금만 실행되고 입금이 수행되지 않는다면 이는 transaction이라 부를 수 없습니다. 

 

그럼 atomic하지 않은 transaction에서는 어떤 문제가 발생할까요?

다시 위의 예제를 가져와보겠습니다.

해당 예제에서 출금만 실행되고 입금이 실행되지 않는다면 돈이 증발해버리는 심각한 문제가 발생할 것입니다.

이러한 문제를 방지하기 위해 출금과 입금을 하나의 transaction으로 정의하고 입금이 실행되지 않으면 출금 또한 취소해버림으로써(Rollback) 출금과 입금 과정을 마치 하나의 작업처럼 실행시킵니다.  

Consistency

두번째 특징은 바로 일관성(consistency)입니다. 

 

Transaction이 수행되기 전과 수행된 후의 데이터는 모두 일관성을 보장해야합니다.

 

데이터의 일관성이란 DBMS에서 제공하는 제약조건을 어기지 않고 데이터의 논리적 오류가 없음을 의미합니다.

송금 예제에서, A가 만원을 이체했는데 B에서 오천원만 받았다면 이는 논리적 오류라고 볼 수 있을 것입니다.

 

transaction은 위에서 이야기한 데이터 일관성을 보장해야합니다. 

Isolation

여러 transaction이 동시에 실행될 때 하나의 transaction은 다른 transaction에게 영향을 받거나 주어서는 안됩니다. 

 

다시 송금 예제를 가져와보겠습니다.

A 계좌에서 B계좌로 이체하는 transaction이 실행되는 도중,

A 계좌의 모든 잔액을 출금하는 transaction이 실행되었다고 가정해보겠습니다.

만약 첫 번째 transaction이 A의 계좌에서 이체 금액을 출금한 상태에서 두 번째 transaction이 잔액을 조회하고 이를 출금하려고한다면 문제가 발생할 수 있습니다.

아직 첫 번째 transaction은 완료된 상태가 아니기 때문에 만약 도중에 문제가 생겨 이체가 취소된다면 두 번째 transaction이 조회한 금액은 잘못된 금액(이체 금액이 빠져있는)이기 때문입니다.

 

이러한 문제를 해결하는 방법은 table, 혹은 해당 row에 lock을 걸어버리는 것이지만 이러한 방법은 동시성을 떨어뜨려 성능에 악영향을 미칠 수 있습니다.

Durability

Transaction이 완료(Commit)된 이후의 데이터는 소프트웨어 혹은 하드웨어의 장애가 발생하더라도 보존되어야한다는 성질.

 

Isolation level

Isolation level은 transaction이 가지는 isolation의 정도를 세분화하여 필요에 따라 이를 적용하여 성능을 높이기위한 방법입니다.

 

Isolation level은 다음과 같이 나눠집니다.

  1. READ UNCOMMITTED
  2. READ COMMITTED
  3. REPEATABLE READ
  4. SERIALIZABLE

각각의 isolation level은 아래로 갈수록 isolation은 정도는 강해지지만 반대로 동시성은 떨어집니다.

 

각각의 isolation level을 설명하기 위해 다음과 같은 계좌(account) table을 정의하였습니다.

mysql> SELECT * FROM account;
+------+--------+
| id   | amount |
+------+--------+
| A    |   1000 |
| B    |      0 |
+------+--------+

여기서도 송금 예제를 통해 설명드리도록 하겠습니다.

 

READ UNCOMMITTED

아직 Commit되지 않은 데이터에 접근할 수 있는 isolation level

 

언제든 데이터에 접근할 수 있기 때문에 Dirty Read가 발생할 수 있습니다.

 

계좌 이체를 위한 transaction이 A의 계좌에서 이체 금액을 출금한 상태에서 또 다른 transaction이 A의 계자를 조회했다고 가정해 보겠습니다. 

하지만 계좌 이체 도중에 문제가 발생하여 해당 과정이 rollback되면 두 번째 transaction은 잘못된 데이터를 조회한 것이 되어버립니다.

이러한 경우를 Dirty Read가 발생했다고 합니다.

 

#계좌 이체 transaction
mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
mysql> START TRANSACTION;
mysql> UPDATE account SET amount = 0 WHERE id = 'A';

#계좌 조회 transaction
mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
mysql> START TRANSACTION;
mysql> SELECT * FROM account;
#아래의 값은 이후 Rollback이 되기 때문에 잘못된 값
+------+--------+
| id   | amount |
+------+--------+
| A    |      0 |
| B    |      0 |
+------+--------+

#계좌 이체 transaction에 문제가 발생되서 rollback
mysql> ROLLBACK;

READ COMMITTED

위의 Drity Read를 해결하기 위해 Commit된 데이터만 접근할 수 있는 isolation level

 

Dirty Read 문제는 해결했지만 해당 isolation level에서는 Non-Repeatable Read문제가 발생할 수 있습니다.

 

첫 번째 transaction이 계좌를 조회한 뒤 두번째 transaction이 A 계좌에서 출금을 수행했다고 가정해보겠습니다.

문제는 첫 번째 transaction이 다시 계좌를 조회했을 때 처음 조회했을 때와 다른 값이 조회된다는 것입니다.

이러한 현상을 Non-Repeatable read라 합니다.

 

#첫 번째 transaction
mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
mysql> START TRANSACTION;
mysql> SELECT * FROM account;
+------+--------+
| id   | amount |
+------+--------+
| A    |   1000 |
| B    |      0 |
+------+--------+

#두 번째 transaction
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
mysql> start transaction;
mysql> UPDATE account SET amount = 1000 WHERE id = 'A';
mysql> COMMIT;

#첫 번째 transaction이 다시 조회하면 non-repeatable read 발생
mysql> SELECT * FROM account;
+------+--------+
| id   | amount |
+------+--------+
| A    |      0 |
| B    |      0 |
+------+--------+

REPEATABLE READ

Non-Repeatable Read의 문제를 해결한 isolation level

위의 문제는 해결했지만 여기서는 Phantom Read라는 새로운 문제가 발생할 수 있습니다.

 

이번에는 송금이 아니라 새로운 사용자를 추가하는 transaction과 모든 사용자의 계좌 정보를 조회하는 transaction이 동시에 실행된다고 가정해보겠습니다.

먼저 계좌 정보를 조회하는 transaction이 해당 정보를 조회하고 그 이후에 다른 transaction이 새로운 사용자를 추가한 다음, 조회 transaction이 다시 한 번 모든 사용자의 정보를 조회하면 처음 조회에서는 존재하지 않았던, 새로운 사용자의 정보가 나타나게 되는데 이러한 문제를 Phantom Read라 합니다.

 

하지만 MySQL에서는 Phantom Read 문제가 발생하지 않습니다.

그 이유는 repeatable read에서 MySQL은 snapshot이라는 것을 통해 데이터를 조회하기 때문입니다.

 

Snapshot이란 임시로 생성된 복사본이라고 이해하시면 될 것 같습니다.

이후에 원본이 변경되더라도 복사본에는 영향을 미치지 않기 때문에

아래와 같은 문제없이 데이터를 조회할 수 있습니다.

#조회 transaction
mysql> SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
mysql> START TRANSACTION;
mysql> SELECT * FROM account;
+------+--------+
| id   | amount |
+------+--------+
| A    |   1000 |
| B    |      0 |
+------+--------+

#사용자 추가 transaction
mysql> SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
mysql> START TRANSACTION;
mysql> INSERT INTO account VALUES('C', 1000);

#조회 transaction에서 다시 사용자 정보를 추가해도 이전과 동일한 정보가 조회
mysql> SELECT * FROM account;
+------+--------+
| id   | amount |
+------+--------+
| A    |   1000 |
| B    |      0 |
+------+--------+

Read committed에서도 snapshot을 이용해 데이터를 조회하는 것은 동일하지만 repeatable read에서는 처음 조회할 때만 snapshot을 생성하는데에 반해 read committed에서는 매번 조회할 때마다 snapshot을 생성합니다.

SERIALIZABLE

가장 높은 isolation level

그만큼 데이터 무결성을 보장할 수 있지만 동시성이 매우 떨어진다는 단점도 가지고 있습니다.

 

MySQL에서는 autocommit이 비활성화 되어있을 경우 SELECT 명령을 실행시킬 때 자동으로 해당 명령어를 SELECT ... FOR SHARE로 바꿔서 실행시킵니다. 

 

SELECT ... FOR SHARE 명령문은 조회하고 싶은 데이터에 shared lock을 걸어버립니다. shared lock이 걸린 데이터는 다른 transaction에서 조회는 가능하지만 업데이트나 삭제는 불가능합니다.

#조회 transaction
mysql> SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
mysql> START TRANSACTION;
mysql> SELECT * FROM ACCOUNT;
+------+--------+
| id   | amount |
+------+--------+
| A    |   1000 |
| B    |      0 |
+------+--------+

#업데이트 transaction
mysql> SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
mysql> START TRANSACTION;
#조회 transaction이 commit되거나 rollback될 때까지, 혹은 timeout까지 대기
mysql> UPDATE account SET amount = 0 WHERE id = 'A';

Reference

 

출처 : devonce.tistory.com/22