본문 바로가기
Database

데이터베이스 트랜잭션(Database Transaction)

by WhoamixZerOne 2022. 9. 7.

▣https://saixiii.com/what-is-database/

✔ 트랜잭션

트랜잭션은 데이터베이스의 데이터를 조작하는 작업의 단위로, "쪼갤 수 없는 업무 처리의 최소 단위"를 말한다.

예를 들면, A라는 사람이 B라는 사람에게 1,000원을 지급하고 B가 그 돈을 받은 경우, 이 거래 기록은 더 이상 작게 쪼갤 수가 없는 하나의 트랜잭션을 구성한다. 만약 A는 돈을 지불했으나 B는 돈을 받지 못했다면 그 거래는 성립되지 않는다. 이처럼 A가 돈을 지불하는 행위와 B가 돈을 받는 행위는 별개로 분리될 수 없으며 하나의 거래내역으로 처리되어야 하는 단일 거래이다.

이런 거래의 최소 단위를 트랜잭션이라고 한다. 트랜잭션 처리가 정상적으로 완료된 경우 커밋(commit)을 하고, 트랜잭션 중 일부라도 오류가 발생할 경우 원래 상태대로 롤백(rollback)을 한다.

 

✔ 트랜잭션의 성질(ACID)

ACID는 하나의 트랜잭션의 안전성을 보장하기 위해 필요한 성질이다.

  • 원자성(Atomicity), All or Nothing
    • 트랜잭션의 모든 연산들은 정상적으로 전부 수행 완료되거나 아니면 어떠한 연산도 전부 수행되지 않은 상태를 보장해야 한다.
    • 위에서 예시로 설명한 거래 기록 내용이 원자성의 성질에 대한 내용이다.
  • 일관성(Consistency)
    • 트랜잭션의 수행 전과 후가 일관된 상태를 유지해야 한다.
    • 데이터베이스의 무결성 제약조건에 위배되지 않아야 한다.
    • 예를 들면, 사용자는 유니크(Unique)한 이메일을 가지고 있어야 하는데 이메일이 없거나 이메일을 지우거나 하는 행위는 무결성 제약조건에 위배되는 행동으로 일관성이 무너질 수 있다.
  • 고립성(Isolation)
    • 하나의 트랜잭션이 실행하는 도중에 변경한 데이터는 이 트랜잭션이 완료될 때까지 다른 트랜잭션이 참조하지 못한다.
    • 예를 들면, A통장에 5,000원 B, C통장에 0원이 있을 때 A통장에서 B통장에 3,000원을 입금하는 중에 B통장에서 C통장으로 2,000원을 입금하는 상황이 발생하면 안 된다. A→B 처리가 끝난 후에 접근해야 한다.
  • 지속성(Durability)
    • 완료(commit)된 트랜잭션은 영구적으로 보존되어야 한다.
    • 계좌 이체가 완료된 이후에 은행에서 문제가 생겼더라도 이미 저장된 데이터는 보존되어야 한다.
    • 계좌 이체가 완료되기 전에 문제가 생겼다면 원자성의 원칙에 따라 이전 상태로 돌아가야 한다.

 

✔ 트랜잭션의 상태

https://github.com/WeareSoft/tech-interview/blob/master/contents/db.md#%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98%EC%9D%B4%EB%9E%80

  • 활동(Active)
    • 트랜잭션이 실행 중에 있는 상태, 연산들이 정상적으로 실행 중인 상태
  • 장애(Failed)
    • 트랜잭션의 실행 중에 장애나 오류가 발생하여 정상적인 실행을 더 이상 할 수 없는 상태
  • 철회(Aborted)
    • 트랜잭션이 장애나 오류로 실패하여 롤백(Rollback) 연산을 수행하여 트랜잭션 실행 이전 데이터로 돌아간 상태
  • 부분 완료(Partially Committed)
    • 트랜잭션이 마지막 연산을 실행시킨 직후의 상태를 의미하며, 커밋(Commit) 연산이 실행되기 직전의 상태
  • 완료(Committed)
    • 트랜잭션이 성공적으로 종료되어 커밋(Commit) 연산을 실행한 후의 상태

 

부분 완료(Partially Committed)와 완료(Committed)의 차이점

커밋 이전의 연산들이 정상적으로 실행되고, 커밋 요청이 들어오면 상태는 부분 완료 상태가 된다.

이후 커밋도 문제없이 실행되고 나면 완료 상태로 전이되고, 만약 오류가 발생하면 장애 상태가 된다.

 

✔ 트랜잭션의 격리 수준(Isolation-level)

트랜잭션끼리의 고립성을 나타내는 것이다.

즉, 특정 트랜잭션이 다른 트랜잭션에 변경한 데이터를 볼 수 있도록 허용할지 말지를 결정하는 것이다.

 

트랜잭션의 격리 수준의 종류

아래와 같이 4가지의 종류가 있다.

  • Read Uncommitted (Level 0)
  • Read Committed (Level 1)
  • Repeatable Read (Level 2)
  • Serializable (Level 3)

레벨이 높을수록 트랜잭션 간의 고립 정도가 높아지고, 성능이 떨어진다.

하지만, Serializable 격리 수준이 아니라면 성능 저하가 크게 차이 나지 않는다고 한다.

"Read Uncommitted"는 일반적인 데이터베이스에서는 거의 사용하지 않고 "Serializable" 역시 동시성이 중요한 데이터베이스에서는 거의 사용되지 않는다. MySQL은 Repeatable Read로 기본 설정이 되어 있다.

아래의 명령어 중 하나로 확인할 수 있다
mysql> SHOW VARIABLES like '%isolation';
mysql> SELECT @@GLOBAL.transaction_isolation;
mysql> SELECT @@SESSION.transaction_isolation;

 

낮은 단계의 격리 수준에 발생하는 현상

Isolation-level Dirty Read Non-Repeatable Read Phantom Read
Read Uncommitted O O O
Read Committed - O O
Repeatable Read - - O(InnoDB 발생 X)
Serializable - - -

1) Dirty Read

  • 어떤 트랜잭션에서 아직 실행이 끝나지 않은 상태임에도 불구하고 다른 트랜잭션에서 볼 수 있는 현상

2) Non-Repeatable Read

  • 한 트랜잭션에서 동일한 쿼리를 두 번 실행할 때 그 사이에 다른 트랜잭션이 값을 수정 또는 삭제함으로써 두 쿼리의 결과가 상이하게 나타나는 비 일관성 현상("Repeatable Read" 정합성에 어긋나는 현상)

3) Phantom Read

  • 한 트랜잭션에서 동일한 쿼리를 두 번 실행할 때 첫 번째 쿼리에서 없던 레코드(유령 Phantom)가 두 번째 쿼리에서 나타나는 현상

 

✔ Read Uncommitted (Level 0)

  • SELECT 문장이 실행하는 동안 해당 데이터에 Shared Lock이 걸리지 않는 Level
  • 아직 커밋되지 않은 데이터를 다른 트랜잭션이 읽는 것을 허용
  • 데이터베이스의 일관성을 유지할 수 없다

아이디가 1인 tx(Transaction)가 먼저 시작하고 이후 아이디가 2인 tx가 시작한 후에 tx1이 insert 한 후에 아직 커밋하지 않은 상태에서도 tx2는 id가 2인 레코드를 조회할 수 있습니다. 이처럼 트랜잭션의 변경내용이 커밋이나 롤백에 상관없이 다른 트랜잭션에 보이는 것을 Dirty Read라고 한다.

하지만 tx1에서 insert 후 오류로 인해 롤백이 된다면 tx2에서는 존재하지 않는 데이터로 계속 처리를 진행하는 문제가 발생합니다.

이러한 문제 등으로 인해 RDBMS 표준에서는 트랜잭션의 격리 수준으로 인정하지 않을 정도로 정합성에 문제가 많다.

 

✔ Read Committed (Level 1)

  • SELECT 문장이 실행하는 동안 해당 데이터에 Shared Lock이 걸리는 Level
  • 트랜잭션이 수행되는 동안 다른 트랜잭션이 접근할 수 없어 대기하게 된다
  • 커밋이 완료된 트랜잭션만 조회할 수 있다

READ Committed에서는 Dirty Read 현상이 발생하지 않는다. 어떠한 트랜잭션에서 데이터를 변경하더라도 커밋이 완료된 데이터만 다른 트랜잭션에서 조회할 수 있기 때문이다.

 

위의 처리는 다음과 같이 된다.

tx1에서 "jang"을 "kim"으로 변경하면 레코드에는 즉시 반영이 되고, 이전 값인 "jang"은 Undo 영역으로 백업이 된다. 커밋이 완료되기 전에 tx2에서 조회하면 Undo 영역의 백업된 레코드에서 결과 값을 가져와 "jang"을 가지고 온다. 말했다시피 커밋이 완료되기 전까지는 다른 트랜잭션에서 변경 내역을 조회할 수 없기 때문이다. 커밋이 완료된 후에야 "kim"을 결과 값을 가져올 수 있다.

Undo 영역
Undo 레코드는 InnoDB의 시스템 테이블 스페이스의 Undo 영역에 기록이 되는데, 트랜잭션의 격리 수준을 보장하기 위한 용도뿐 아니라 트랜잭션의 롤백에 대한 복구에도 사용이 된다.

Undo 영역 사용 용도
1. 트랜잭션의 롤백 대비용
2. 트랜잭션의 격리 수준을 유지하면서 높은 동시성을 제공

하지만 READ Committed에서는 Non-Repeatable Read 현상이 발생한다.

위의 그림 tx2에서 동일한 SELECT문을 실행했을 때 항상 같은 결과를 보장해야 하는 Repeatable Read 정합성에 어긋나는 결과 값을 가져온다.

 

✔ Repeatable Read (Level 2)

  • 트랜잭션이 완료될 때까지 SELECT 문장이 실행하는 모든 데이터에 Shared Lock이 걸리는 Level
  • 트랜잭션 범위 내에서 조회한 데이터의 내용이 항상 동일함을 보장

Repeatable Read에서는 Dirty Read, Non-Repeatable Read 현상은 발생하지 않는다.

Repeatable Read는 트랜잭션 시작되기 전에 커밋된 데이터에 대해서만 조회할 수 있는 격리 수준이고 InnoDB의 트랜잭션의 자시만의 고유한 트랜잭션 번호(순차적으로 증가)를 가지고 있는데, 자신보다 낮은 트랜잭션 번호에서 변경된 커밋된 볼 수 있다.

 

위의 처리는 다음과 같이 된다.

tx1이 SELECT 구문으로 "jang"을 조회하고 tx2에서 "jang"을 "kim"으로 변경한 후에 커밋을 한다. 이후에 tx1에서 다시 조회해도 자신보다 낮은 트랜잭션 번호에서 변경한 것만 볼 수 있기 때문에 Undo영역에 있는 "jang"을 조회한다.

Undo 영역에는 하나 이상의 레코드가 존재할 수 있는데, 하나의 트랜잭션 실행시간이 길어질수록 Undo에 백업된 레코드가 많아져서 멀티 버전(MVCC)을 관리해야 하는 단점이 있다.

그리고 Repeatable Read에서는 Update 부정합, Phantom Read 현상이 발생할 수 있다.

 

Update 부정합

START TRANSACTION; -- transaction id : 1
SELECT * FROM users WHERE name='jang';

    START TRANSACTION; -- transaction id : 2
    SELECT * FROM users WHERE name='jang';
    UPDATE users SET name='kim' WHERE name='jang';
    COMMIT;

UPDATE users SET name='lee' WHERE name='jang'; -- 0 row(s) affected
COMMIT;

tx2에서 "kim"으로 변경하고 "jang"은 Undo 영역에 기록해서 tx1의 정합성을 보장한다.

마지막에 tx1에서 UPDATE문을 실행하는데 UPDATE의 경우 변경을 수행할 로우에 대해 잠금이 필요하다. 하지만 "jang"은 레코드 데이터가 아닌 Undo 영역 데이터이고, Undo 영역에 있는 데이터는 쓰기 잠금을 할 수 없다.

그러므로 UPDATE 구문은 레코드에 대해 쓰기 잠금을 시도하지만 레코드는 존재하지 않으므로 아무 변경도 일어나지 않고, 결과적으론 "lee"가 아닌 "kim"인 상태이다.

 

Phantom Read

START TRANSACTION; -- transaction id : 1 
SELECT * FROM users; -- 0건 조회

    START TRANSACTION; -- transaction id : 2
    INSERT INTO users VALUES('hong');
    COMMIT;

SELECT * FROM users; -- 여전히 0건 조회 
UPDATE users SET name='jang' WHERE id=1; -- 1 row(s) affected
SELECT * FROM users; -- 1건 조회 
COMMIT;

하나의 트랜잭션에서 동일한 쿼리를 두 번 실행했을 경우, 첫 번째 쿼리에서 없던 레코드가 두 번째 쿼리에서 나타나는 현상으로, INSERT에 대해서만 발생하는 문제다.(SELECT, DELETE에 대해서는 발생하지 않는다)

 

✔ Serializable (Level 3)

  • 트랜잭션이 완료될 때까지 SELECT 문장이 실행하는 모든 데이터에 Shared Lock이 걸리는 Level
  • 가장 엄격한 격리 수준으로 완벽한 읽기 일관성 모드를 제공
  • 다른 사용자는 그 영역에 해당되는 데이터에 대한 수정 및 입력이 불가능하다

 

 

🔗 Reference

 

 

 

 

 

 

'Database' 카테고리의 다른 글

Ubuntu MySQL 8.0 설치  (0) 2023.08.26
MySQL 유저 계정 생성  (0) 2023.05.05
MySQL 트랜잭션 격리 수준(Isolation level) 확인하기  (1) 2022.09.20

댓글