티스토리 뷰

 

  • 목표
    • 트랜잭션의 ACID 속성에 대해 설명할 수 있다.
    • 트랜잭션의 Commit/Abort 기능이 애플리케이션에 주는 이점을 설명할 수 있다.
    • 동시성 문제의 종류를 이해하고 이를 방지할 수 있는 격리 수준의 종류에 대해 설명할 수 있다.

 

Transaction

트랜잭션: 애플리케이션에서 여러 개의 읽기와 쓰기를 하나의 논리적 단위로 묶는 방법, 한 연산으로 실행 됨 전체가 성공(커밋) 하거나 실패(어보트) 함

트랜잭션을 사용함으로써 잠재적인 오류 시나리오와 동시성 문제를 무시할 수 있음(all or nothing, 부분적 결과를 허용하지 않음)

트랜잭션의 핵심 기능은 오류가 생기면 어보트되고 안전하게 재시도 할 수 있다는 점.

ACID?

너무 흔하게 들어보는 개념이지만. .DB 마다 ACID 구현이 제각각이다 ..

원자성(Atomicity): 어보트 능력(abortability) 에 더 가깝다, 여러 읽기/쓰기 작업이 하나의 원자적인 트랜잭션으로 묶이게 되고, 작업이 완료(커밋)되지 못하면 어보트됨 → 애플리케이션 입장에서는 트랜잭션이 어보트 되었다면 이 트랜잭션이 어떤 것도 변경하지 않았음을 알 수 있음. 애플리케이션이 부분 실패를 고려할 필요가 없게 해줌.

일관성(Consistency): 항상 진실이어야 하는 데이터에 관한 정의(불변식)이 있다는 것(e.g. 회계 시스템에서 모든 계좌의 대변과 차변은 항상 맞아떨어져야 함) → BUT 이것은 애플리케이션의 속성에 가까움 DB는 불변식을 위반하는 데이터를 막을 수는 있지만 (e.g. 외래키 재약 조건 ..), 일관성을 지키기 위해 DB만으로 되는 것이 아님

격리성(Isolation): 동시에 실행되는 트랜잭션은 서로 격리됨. 한 트랜잭션은 다른 트랜잭션의 내용을 전부 볼 수 있던지 아니면 아무것도 볼 수 없든지 둘 중 하나여만 한다. 직렬성 과 유사한 의미로, 여러 트랜잭션이 동시에 실행되더라도 트랜잭션이 순차적으로 실행된 결과와 동일함을 보장함. 추후에 다룰 격리수준에서 더 자세히 설명 예정.

지속성(Durability): 트랜잭션이 성공적으로 커밋됐다면 하드웨어 결함이 발생했거나 데이터베이스가 죽더라도 기록한 모든 데이터는 손실되지 않는다는 보장. 물론 완벽한 지속성은 존재하지 않는다 (하드웨어와 백업이 동시에 파괴되어 버릴 경우..)

Isolation Level

각 격리 수준은 트랜잭션 간 방지해야 하는 현상에 따라 정의됨

Preventable Read Phenomena by Isolation Level

Isolation Level Dirty Read Nonrepeatable Read(fuzzy read) Phantom Read

Read uncommitted Possible Possible Possible
Read committed Not possible Possible Possible
Repeatable read Not possible Not possible Possible
Serializable Not possible Not possible Not possible
  • Dirty Read: 다른 트랜잭션에 의해 변경된 커밋되지 않은 데이터를 읽음
  • Non-Repeatable Read: 같은 쿼리를 동일 트랜잭션 내에서 두 번 실행했을 때 결과가 달라지는 현상 (다른 트랜잭션에 의해 데이터가 수정되고 커밋되었을 경우)
  • Phantom Read: 어떤 트랜잭션에서 실행한 쓰기가 다른 트랜잭션의 검색 질의 결과를 바꾸는 현상

Read committed

가장 기본적인 수준의 트랜잭션 격리로, 두 가지를 보장함.

  • 더티 읽기 방지: 데이터베이스에서 읽을 때 커밋한 데이터만 보게 됨
  • 더티 쓰기 방지: 데이터베이스에 쓸 때 커밋한 데이터만 덮어쓰게 됨

오라클 11g 기본 설정

트랜잭션 1이 쿼리 도중 트랜잭션 2가 데이터를 변경하거나 추가해서 커밋하게 되면, 쿼리 결과가 달라지는 fuzzy read나, 없던 row가 튀어나오는 phantom read 가 생길 수 있음

데이터베이스는 로우 수준 잠금 을 이용해 더티쓰기를 방지

오라클 기준 Row lock(TX lock) 이용, INSERT, UPDATE, DELETE, MERGE, or SELECT ... FOR UPDATE  수행 시 해당 row 에 대한 잠금을 획득하여 트랜잭션이 커밋되거나 어보트 될 때까지 잠금을 보유

오직 한 트랜잭션만 잠금을 보유할 수 있고 다른 트랜잭션이 쓰기를 원한다면 잠금을 보유한 트랜잭션이 커밋되거나 어보트 된 후에 잠금을 얻을 수 있음

오라클 기준 다른 트랜잭션이 변경 중이면 그 변경 전 버전을 Undo Log 를 이용해 읽음

Repeatable read

스냅샷 격리라고도 함.

각 트랜잭션은 데이터베이스의 일관된 스냅샷 으로부터 읽음

데이터가 나중에 다른 트랜잭션에 의해 바뀌더라도 각 트랜잭션은 특정한 시점의 과거 데이터를 봄

스냅샷 격리의 핵심 원리는 읽는 쪽에서 쓰는 쪽을 차단하지도 않고, 쓰는 쪽에서 읽는 쪽을 차단하지도 않는다는 것

스냅샷 격리는 MVCC(multi-version concurrency control) 로 구현

mySQL InnoDB 에서는 트랜잭션 ID + undo log 를 사용해서 구현되고,

각 row 마다 3개의 field 가 저장된다

  • DB_TRX_ID: insert나 update 를 수행한 가장 마지막 트랜잭션 id
  • DB_ROLL_PTR: undo log 의 레코드를 가리킴, update 전의 내용을 다시 가져오는데 필요한 정보가 포함
  • DB_ROW_ID: 새 행이 삽입될 때 증가되는 행 id

 

 

트랜잭션이 update나 delete 를 하면 변경 전 데이터는 undo log 에 저장되고, 현재 데이터는 덮어씀. 다른 트랜잭션이 자기보다 나중에 커밋된 데이터를 보게 되면 undo log 를 참조해서 트랜잭션 시작 시점에 맞게 버전을 재구성

undo log는 더이상 이전 버전의 row를 빌드하는데 필요한 스냅샷이 할당된 트랜잭션이 없는 경우에 삭제됨.

스냅숏 격리는 각 데이터베이스에서 다른 이름을 사용, 오라클에서는 직렬성, postgreSQL 과 mySQL 에서는 반복 읽기(repeatable read) 하고 함

갱신 손실 문제

두 트랜잭션이 동시에 쓰기를 실행한다면? 두 트랜잭션이 데이터베이스에서 값을 읽고 변경한 후, 변경한 값을 다시 쓴다면 (e.g. 카운터를 증가시킴), 동시에 트랜잭션 작업이 이루어질 경우 두번째 변경이 첫번째 변경을 포함하지 않으므로 변경 중 하나가 손실될 수 있음

이 문제를 해결하는 방법?

  • 명시적 잠금: 데이터 수정 전에 SELECT .. FOR UPDATE 를 이용하여 질의에 의해 반환된 모든 로우에 대해 Exclusive Lock (배타 락) 을 건다
    • 오라클 Lock 모드
      • Shared lock mode: 여러 트랜잭션이 동시에 공유 잠금을 획득할 수 있음, 공유 잠금이 걸린 데이터에 대해 쓰기 작업은 불가능(shared lock 이 걸릴 경우 exclusive lock은 획득 불가). 보통은 일관된 읽기를 제공하는 MVCC 로 해결
      • Exclusive lock mode: 리소스를 배타적으로 잠그는 첫번째 트랜잭션은 배타적 잠금이 해제될 때까지 데이터를 변경할 수 있는 유일한 트랜잭션임, 다른 트랜잭션이 변경하려고 하면 blocking 됨. 단 MVCC 때문에 이전 데이터는 읽을 수 (SELECT) 있음
  • 갱신 손실 자동 감지: 스냅숏 격리와 결합해 Oracle 의 경우 갱신 손실이 발생하면 감지하여 문제가 되는 트랜잭션을 어보트 시킴

 

왼: Session1, 오: Session2

Session2의 트랜잭션 시작 이후 Session1이 같은 데이터에 대해 수정해서 커밋했기 때문에,

Session2가 같은 데이터에 대해 UPDATE 시 ORA-08177: can't serialize access for this transaction 에러가 발생

애플리케이션에서 특별한 데이터베이스 기능을 사용하지 않아도 갱신 손실이 자동 감지되기 때문에 오류가 덜 발생하게 해줌

Oracle Database permits a serializable transaction to modify a row only if changes to the row made by other transactions were already committed when the serializable transaction began. The database generates an error when a serializable transaction tries to update or delete data changed by a different transaction that committed after the serializable transaction began:

ORA-08177: Cannot serialize access for this transaction

쓰기 스큐와 팬텀

쓰기 스큐: 두 트랜잭션이 동시에 조건을 확인하고 충돌 없이 각자 업데이트했지만, 전체적으로 무결성이 깨지는 상황

팬텀:

단순히 새 행 삽입이 아니라 아래 조건 역시 팬텀

변화 유형 예

INSERT 새로 조건을 만족하는 행 삽입됨
UPDATE 기존 행이 조건을 만족하도록 바뀜
DELETE 조건 범위의 행이 사라져서 COUNT 등 조건이 달라짐

아래 예시와 같이 동시에 같은 조건의 행을 읽고, 각각 조건을 만족한다고 판단 후 수정

결과적으로 트랜잭션 도중 다른 트랜잭션에 의해 조건의 결과가 깨지게 됨

 

 

쓰기 스큐는 두 트랜잭션이 같은 객체를 읽어서 그 중 일부를 갱신할 때 나타날 수 있음

쓰기 스큐 방지 방법?

  • Next Key Lock: MySQL InnoDB 엔진에서 REPEATBLE READ 격리 수준에 사용되는 방식으로 인덱스의 레코드 (record lock) + 레코드의 바로 인접한 사이의 간격(gap lock) 을 잠궈 팬텀을 방지
    • 색인 범위 잠금이란? 색인(인덱스)을 기준으로 특정 범위에 대해 공유 잠금 또는 배타 잠금을 거는 방식
//users 테이블에 age 로 index 가 설정되었다고 가정
SELECT * FROM users WHERE age BETWEEN 20 AND 30 FOR UPDATE;
// InnoDB는 age 인덱스를 따라 20 ~ 30 사이 각 row + 그 사이의 gap에 대해
// next key lock을 걸게됨
// 다른 트랜잭션은 해당 범위 안 데이터에 대해 INSERT, UPDATE, DELETE 를 할 수 없음
  • 직렬성 격리수준 이용

 

 

Serializable

직렬성 트랜잭션을 구현하는 3가지 방법

  • 트랜잭션 순서대로 실행하기: 각 트랜잭션 실행 시간이 짧고 트랜잭션 처리량이 단일 CPU 코어에서 처리할 수 있을 정도로 처리량 낮아야 함
  • 2단계 잠금(Two-Phased Locking): 직렬성을 구현하는 가장 표준적인 방법. 비관적(pessimistic) 동시성 제어로 다음과 같이 동작
    • 트랜잭션 A가 객체 하나를 읽고 트랜잭션 B가 쓰기를 원한다면 B는 A가 커밋되거나 어보트 될 때까지 기다려야 한다
    • 트랜잭션 A가 객체를 쓰고 트랜잭션 B가 객체를 읽기 원한다면 마찬가지로 B는 A가 커밋되거나 어보트 될 때까지 기다려야 한다
    읽는 쪽, 쓰는 쪽을 둘다 막는 방식은 각 객체에 잠금을 이용해 구현동시성이 줄어드는 단점, 객체에 쓴다면 배타 락, 객체를 읽는다면 공유 락을 사용 (동시에 여러 트랜잭션이 공유 락을 획득하는 것은 허용)
  • 직렬성 스냅숏 격리(SSI): 낙관적 동시성 제어 과거의 읽기에 영향을 미치는 쓰기 감지 & 오래된 MVCC 버전을 읽었는지 감지, 트랜잭션이 커밋을 원할 때 실행이 직렬적이지 않으면 어보트 시킴기본적으로 스냅샷 격리를 실행하지만 트랜잭션 간의 읽기-쓰기 충돌을 모니터링하여(트랜잭션 간 의존성 그래프를 이용) 트랜잭션 그래프에서 동시에 실행되는 트랜잭션의 anomaly 를 식별하고 anomaly가 발생하지 않도록 트랜잭션을 롤백
    PostgreSQL 의 Serializable 에서 구현됨
    읽기는 쓰기를 차단하지 않고, 쓰기도 읽기를 차단하지 않음 → 2PL 보다 성능 우수
//session 1
BEGIN;
SELECT SUM(value) FROM mytab WHERE class = 1;

INSERT INTO mytab VALUES (2, 30);

//session 2
BEGIN;
SELECT SUM(value) FROM mytab WHERE class = 2;
//session 1에서 읽은 데이터를 session 2에서 변경
INSERT INTO mytab VALUES (1, 300);
//session2 에서 정상적으로 커밋
COMMIT;

//session 1
//session 1에서 커밋 시, ERROR 발생
//트랜잭션을 커밋하려고 할 때 데이터베이스는 무시된 쓰기 중 커밋된게 있는지 확인 후
//커밋된 것이 있으면 어보트 시킴
COMMIT;
ERROR:  could not serialize access
        due to read/write dependencies
        among transactions
DETAIL:  Cancelled on identification
         as a pivot, during commit attempt.
HINT:  The transaction might succeed if retried.

 

 

 

 

 

출처:

https://docs.oracle.com/cd/E28271_01/server.1111/e25789/consist.htm#BABEIHGJ

https://dev.mysql.com/doc/refman/8.4/en/innodb-multi-versioning.html

https://dev.mysql.com/blog-archive/mysql-8-0-mvcc-of-large-objects-in-innodb/

https://docs.oracle.com/cd/E17952_01/mysql-5.7-en/innodb-next-key-locking.html#:~:text=To prevent phantoms%2C InnoDB uses,the index records it encounters.

https://wiki.postgresql.org/wiki/SSI

https://wiki.postgresql.org/wiki/Serializable#Cahill2009

데이터 중심 애플리케이션 설계  7장

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/02   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
글 보관함