kimyu0218
  • [DB/쉬운코드] 트랜잭션과 동시성 제어 (ACID/이상 현상)
    2024년 05월 03일 17시 06분 02초에 업로드 된 글입니다.
    작성자: @kimyu0218

    트랜잭션

    J가 H에게 20만원을 이체하는 상황을 SQL문으로 작성해보면 위와 같다. 이체 작업은 두 SQL문이 모두 성공해야만 성공한다. 즉, 하나라도 실패해서는 안된다.

     

    트랜잭션은 하나의 논리적인 작업 단위를 의미한다. 즉, 논리적인 이유로 여러 SQL문을 단일 작업으로 묶어서 처리해야 하는 것을 의미한다. 위 이체 작업을 트랜잭션으로 처리하면 일부만 성공해서 DB에 반영되는 일이 일어나지 않는다.

    START TRANSACTION; /* 트랜잭션의 시작 (= AUTOCOMMIT 비활성화) */
    UPDATE account SET balance = balance - 200000 WHERE id = 'J';
    UPDATE account SET balance = balance + 200000 WHERE id = 'H';
    COMMIT; /* 트랜잭션의 종료 : 지금까지의 작업을 DB에 영구적으로 저장한다 */
    `COMMIT` `ROLLBACK`
    지금까지의 작업 내용 DB에 반영 지금까지의 작업 내용 모두 취소
    💡AUTOCOMMIT
    • 단일 SQL문을 자동으로 트랜잭션으로 처리한다.
    • MySQL에서 AUTOCOMMIT은 기본으로 enabled 되어 있다.

     

    ACID

    atomicityall or nothing을 의미한다. 트랜잭션은 논리적으로 쪼갤 수 없는 작업 단위이기 때문에 내부의 SQL문이 모두 성공해야 한다. (commit) 만약 중간에 SQL문이 실패하면 지금까지의 작업을 모두 취소하여 아무 일도 없었던 것처럼 되돌려야 한다. (rollback)

     

    J가 H에게 100만원을 추가로 이체하는 상황을 가정해보자. J의 계좌에 80만원 밖에 남아있지 않기 때문에 제약 사항에 의해 100만원을 이체할 수 없다. consistency는 데이터베이스의 일관성을 유지한다. 트랜잭션이 데이터베이스에 정의된 규칙을 위반했다면 rollback 하여 consistent한 상태를 유지한다.

     

    이번에는 J가 H에게 20만원을 이체할 때, H도 본인 계좌에 30만원을 입금하는 상황을 가정해보자. 위처럼 트랜잭션을 동시에 실행하면 30만원이 사라지는 문제가 발생한다. isolation은 여러 트랜잭션들이 동시에 실행될 때도 혼자 실행하는 것처럼 동작하게 만든다. DBMS는 여러 종류의 isolation level을 제공하는데, 개발자는 이를 수정하여 트랜잭션을 어떤 레벨로 동작시킬지 설정할 수 있다.

    높은 isolation level 낮은 isolation level
    동시성 낮음 (= DB 성능 저하) 동시성 높음 (= DB 성능 향상)
    다른 트랜잭션에 영향 받을 가능성이 낮다 다른 트랜잭션에 영향 받을 가능성이 커진다

     

    durabilitycommit된 트랜잭션을 DB에 영구적으로 저장한다. 즉, 데이터베이스 시스템에 문제가 생겨도 commit된 트랜잭션의 결과가 유지되어야 한다.

    ⭐ 트랜잭션
    하나의 논리적인 작업 단위
    1. atomicity : 트랜잭션의 연산은 모두 반영되거나 아예 반영되지 않아야 한다.
    2. consistency : 트랜잭션은 언제나 일관성을 유지해야 한다.
    3. isolation : 둘 이상의 트랜잭션이 동시에 수행될 때, 다른 트랜잭션의 연산에 끼어들 수 없다. (= 한 번에 하나만 실행되는 것처럼)
    4. durability : commit된 트랜잭션은 영구적으로 반영되어야 한다.

    동시성 제어

    concurrency control makes any schedule serializable.

    schedule

    K가 H에게 20만원을 이체할 때 H도 본인 계좌에 30만원을 입금하는 상황을 가정해보자. 이는 여러 형태로 실행될 수 있다.

    • 이체 작업 종료 후 입금 작업 시작 (serial)
    • 입금 작업 종료 후 이체 작업 시작 (serial)
    • 작업 도중에 다른 작업 시작 (nonserial)

    마지막의 경우, 실행 순서에 따라 다음과 같이 이상한 결과를 초래할 수 있다. 이를 lost update라고 부른다.

    schedule은 여러 트랜잭션들이 동시에 실행될 때, 각 트랜잭션에 속한 연산들의 실행 순서를 의미한다. 위의 사례를 schedule로 표현하면 `r1(K) w1(K) r1(H) r2(H) w2(H) c2 w1(H) c1`이다.

    🚨 각 트랜잭션 내의 연산 순서는 변하지 않는다.

     

    serial은 순차적이라는 뜻이다. serial schedule은 트랜잭션이 겹치지 않고 한 번에 하나씩 실행되는 schedule을 의미한다. 반면, nonserial schedule은 트랜잭션이 겹쳐서 실행되는 경우를 말한다.

    serial schedule nonserial schedule
    한 번에 하나의 트랜잭션 실행 트랜잭션 겹쳐서 실행
    항상 정상적으로 동작 실행 순서에 따라 이상한 결과 가능
    동시성 없음 → 성능 저하 동시성 높음 → 성능 향상

    serial schedule은 성능이 낮아 현실적으로 사용할 수 없는 방법이다. 따라서 nonserial schedule로 실행해도 이상한 결과가 나오지 않는 방법을 찾아야 한다.

     

    conflict serializable

    schedule 내의 2개의 연산이 아래 세 가지 조건을 모두 만족하면 conflict라고 한다.

    1. 서로 다른 트랜잭션에 속한다.
    2. 같은 데이터에 접근한다.
    3. 최소 하나는 쓰기 연산을 수행한다. (read-write conflict, write-write conflict)

    conflict 연산은 그 순서가 변하면 결과도 바뀐다는 특징이 있다.

     

    conflict equivalent는 2개의 schedule이 다음 두 조건을 모두 만족하는 상황을 의미한다.

    1. 두 schedule은 같은 트랜잭션을 가진다.
    2. conflict 연산의 순서가 동일하다. 

    conflict serializableserial schedule과 conflict equivalent하는 경우를 말한다. 모든 serial schedule들과 conflict equivalent하지 않다면 해당 schedule은 이상 현상을 초래할 것이다.

     

    recoverability

    위 그림은 `tx1`이 `tx2`가 commit 되기 전에 데이터를 읽고 commit한 상황이다. 하지만 그 후에 `tx2`가 rollback 되었으므로 `tx2`가 변경한 데이터를 읽은 `tx1`도 rollback 되어야 한다. 그러나 durability 속성으로 인해 한 번 commit된 트랜잭션은 rollback 할 수 없다.

     

    이처럼 schedule 내에서 commit된 트랜잭션이 rollback된 트랜잭션이 write했던 데이터를 읽은 상황unrecoverable schedule이라고 한다. `tx2`를 rollback 해도 이전 상태로 복원할 수 없기 때문에 DBMS에서 절대 허용하면 안된다.

     

    따라서 의존성이 있는 트랜잭션의 경우, 의존하는 트랜잭션은 의존되는 트랜잭션이 종료되기 전까지 commit/rollback 해서는 안된다. 이를 recoverable schedule이라고 한다. (= rollback 하면 이전 상태로 온전히 돌아갈 수 있음)

    하지만 여러 트랜잭션의 rollback이 연쇄적으로 일어나면 처리 비용이 많이 든다. (cascading rollback) 따라서 데이터를 write 트랜잭션이 종료된 뒤에 데이터를 읽도록 처리해야 한다. 즉, 어떤 트랜잭션도 commit 되지 않은 트랜잭션이 write한 데이터는 읽을 수 없다. (cascadeless schedule)

     

    하지만 cascadeless schedule에서도 문제가 발생할 수 있다. write 작업이 두 번 이상 발생한 경우, 가장 처음 실행한 write 작업을 rollback 하면 나중에 실행했지만 commit 된 트랜잭션의 결과들이 모두 사라진다. 따라서 write 트랜잭션이 종료되기 전까지 데이터를 쓰지도, 읽지도 않도록 제어해야 한다. (strict schedule)

      복원 다른 tx의 read 다른 tx의 write
    unrecoverable schedule 불가 - -
    schedule with cascading rollback 가능 - -
    cascadeless schedule 가능 X O
    strict schedule 가능 X X

     

    isolation 이상 현상

    데이터의 초기 상태는 `x=10`, `y=20`이며, 1번 트랜잭션이 2번 트랜잭션이 write한 `y`를 읽어 `x`에 더하는 상황이다. 하지만 2번 트랜잭션이 rollback 되면서 `x`가 유효하지 않은 값을 가지게 된다. dirty readcommit 되지 않은 트랜잭션이 write한 결과를 읽었을 때 발생한다. (commit하지 않은 데이터를 읽었다 = read uncommitted)

    위 그림은 하나의 트랜잭션 내에서 같은 데이터를 서로 다른 값으로 읽는 상황이 발생했다. 즉, 트랜잭션이 `x` 데이터를 두 번 이상 읽을 때, 다른 트랜잭션이 commit하여 `x`가 변한 상황이다. (commit한 데이터를 읽었다 = read committed) 이는 non-repeatable read라고 부른다.

     

    non-repeatable read와 유사한 phantom read도 있다. phantom read는 한 트랜잭션에서 같은 데이터를 조회할 때 없던 데이터가 생기는 현상을 의미한다. repeatable read는 MySQL의 기본 isolation level이다. repeatable read는 여러 트랜잭션이 동일한 값에 접근할 때 동일한 값을 보장한다. 하지만 다른 트랜잭션에 실행한 insert 쿼리는 막아주지 못한다. (phatom 레코드!)

    📝 non-repeatable read는 데이터의 수정이나 삭제에 의해, phantom read는 데이터의 삽입에 의해 발생한다. 
    🚨 이외에도 commit 되지 않은 데이터를 write 하는 dirty write, 업데이트를 덮어쓰는 lost update 등이 있다.

     

    위와 같은 이상 현상들이 아예 발생하지 않도록 제어하는 것이 좋지만, 이를 모두 제어하면 제약 사항이 많아져 데이터베이스의 성능이 저하된다. 따라서 일부 이상 현상을 허용하는 isolation level을 만들어서 사용자가 필요에 따라 적절하게 선택할 수 있도록 해야 한다.

      dirty read non-repeatable read phantom read
    read uncommitted O O O
    read committed X O O
    repeatable read X X O
    serializable X X X
    💡 snapshot isolation
    • MVCC의 한 종류
    • snapshot = 특정 시점의 형상
    • snapshot isolation의 특정 시점은 트랜잭션의 시작 시점을 기준으로 한다.
    • write 작업이 발생해도 commit 되기 전까지 snapshot에 기록된다. 즉, 외부에서 봤을 때 데이터가 유지된다.
    • commit이 발생하면 데이터베이스에 변경 내역이 반영된다. 단, write-conflict가 발생하면 이번에 commit하려는 내용은 버린다. (First-committer win)
    🚨 RDMS마다 제공하는 isolation level이 다를 수 있고, 같은 이름의 isolation level이라도 동작 방식이 다를 수 있다.

     

    lock

    동시성 제어는 트랜잭션의 isolation 속성을 만족하기 위해 존재하고, 이를 위해 lock을 사용한다. 데이터베이스에서의 lock은 운영체제의 lock과 유사하다. 데이터마다 lock을 가지고 있고, lock을 획득해야 자원에 접근할 수 있다. lock은 크게 두 종류가 있다.

      write-lock read-lock
    alias exclusive lock shared lock
    read X O
    write X X
    ⭐ exclusive lock vs. shared lock
    • DBMS의 동시성을 제어하기 위해 사용되는 lock
    • exclusive lock은 한 번에 하나의 트랜잭션만 해당 데이터에 접근할 수 있다. 주로 write 작업을 할 때 사용된다.
    • shared lock은 여러 트랜잭션이 동시에 같은 데이터를 읽을 수 있도록 허용하지만, 변경하는 것은 허용하지 않는다. 주로 read 작업에서 사용된다.

    하지만 lock만으로 이상 현상을 완벽하게 막을 수는 없다. 따라서 트랜잭션에서 모든 locking 연산이 최초의 unlock 연산보다 먼저 수행되도록 해야 한다. (2PL protocol)

    💡2PL protocol (= two phase locking)
    • expanding phase : lock을 획득하기만 하고 반환하지 않는 phase
    • shrinking phase : lock을 반환하기만 하고 취득하지 않는 phase
    • 즉, 한 번 unlock 한 후에는 lock을 획득하지 않는다.
    🚨 2PL protocol은 serializablility를 보장하지만 deadlock이 발생할 수 있다.

    참고자료

    댓글