트랜잭션과 동시성

soyeon·2023년 5월 10일
post-thumbnail

트랜잭션(Transaction)

트랜잭션은 데이터베이스의 상태를 변환시키기 위해서 논리적 기능을 수행하는 하나의 작업 단위이다.

한꺼번에 모두 수행되어야 할 일련의 데이터베이스 연산이다.

사용자의 시스템에 대한 서비스 요구 시 시스템의 상태 변환 과정의 작업 단위이다.

데이터베이스 시스템에서 병행 제어 및 회복 작업의 논리적 작업 단위이다.

하나의 트랜잭션은 COMMIT 되거나 ROLLBACK 되어야 한다.

ACID

데이터베이스 트랜잭션이 안전하게 수행된다는 것을 보장하기 위한 성질을 가리키는 약어이다.

  • 원자성 (Atomicity)

    • 트랜잭션의 연산은 데이터베이스에 모두 반영되든지 아니면 전혀 반영되지 않아야 한다.

    • 자기의 연산에 대하여 전부(All) 또는 전무(Nothing) 실행만이 존재하며, 일부 실행으로는 트랜 잭션의 기능을 가질 수 없다.

    • 트랜잭션 내의 모든 연산은 반드시 한꺼번에 완료되어야 하며, 그렇지 못한 경우는 한꺼번에 취소되어야 한다.

  • 일관성 (Consistency)

    • 트랜잭션이 그 실행을 성공적으로 완료하면 언제나 일관성 있는 데이터베이스 상태로 변환한다.

    • 시스템이 가지고 있는 고정 요소는 트랜잭션 수행 전과 트랜잭션 수행 완료 후에 같아야 한다.

  • 격리성 (Isolation, 독립성)

    • 둘 이상의 트랜잭션이 동시에 병행 실행되는 경우 하나의 트랜잭션 실행 중에 다른 트랜잭션 의 연산이 끼어들 수 없다.

    • 트랜잭션 T1과 T2에 대해서 T1이 시작되기 전에 T2가 끝나든지, T1이 끝난 후 T2가 시작되든지 해야 한다.

  • 영속성 (Durability, 지속성)

    • 트랜잭션이 일단 그 실행을 성공적으로 완료하면 그 결과는 영속적이어야 한다.

    • 트랜잭션에 의해서 생성된 결과는 계속 유지되어야 한다.

외부 요인에 인해서 트랜잭션이 길어질 수 있는 부분은 트랜잭션을 배제한다.
트랜잭션의 범위는 최대한 짧게 가져간다.
트랜잭션 어노테이션을 활용할 시 프록시 패턴으로 작동하기 때문에

격리레벨

MySQL에서의 격리레벨을 살펴보자.
ISOLATION - 트랜잭션은 서로 간섭하지 않고 독립적으로 동작한다.
격리레벨은 아래의 세가지 조건에 따라서 결정된다.

  • Dirty Read: 커밋되지 않은 데이터를 읽음
  • Non Repeatable Read: 같은 데이터를 조회 시 결과가 다름
  • Phantom Read: 같은 조건으로 데이터를 읽었을 때 없던 데이터가 생기거나 삭제됨
Dirty ReadNon Repeatable ReadPhantom Read
Read UncommittedOOO
Read CommittedXOO
Repeatable ReadXXO
Serializable ReadXXX

Read Uncommitted -> Read Committed -> Repeatable Read -> Serializable Read 순으로 이상현상이 적어지지만 그만큼 동시 처리량이 적어진다.
때문에 중간 단계인 Read Committed, Repeatable Read를 많이 사용한다.

동시성 제어하기

동시성
대부분 하나의 웹 서버는 여러 개의 요청을 동시에 요청할 수 있다. 같은 코드가 동시에 여러 번 작동한다. 이때 하나의 자원을 두고 여러 개의 연산들이 얽혀 데이터 정합성을 깨뜨릴 수 있다.

A를 읽어 '를 뒤에 붙이는 작업을 수행한다고 가정하면
트랜잭션1이 A라는 데이터를 읽어 A'라고 하는 작업을 수행하는 도중에
트랜잭션2가 동시에 A를 읽어 A'라고 한다면
실제로 A''가 되어야하지만 A'가 되어 정합성이 깨진다.

이와 같이 동시에 같은 데이터를 읽거나 업데이트 하는 경우에 발생하는데
이를 해결하기 위해서는 공유 자원에 대한 잠금을 획득하여 트랜잭션을 줄을 세워 하나씩 수행하도록 한다.

동시성 이슈가 어려운 이유
1. 로컬에서는 대부분 하나의 스레드로 테스트한다.
2. 이슈가 발생하더라도 오류가 발생하지 않는다.
3. 코드에서 잘 보이지 않는다.
4. 항상 발생하지 않고 비결정적으로 발생한다.

그래서 개발할 때 작성한 코드가 동시에 수행될 수 있다는 염두를 두면 이슈를 줄일 수 있다.

락(Lock)

동시성 이슈를 줄이기 위해서 잠금을 통해 제어하는 방법을 위에서 알았다.
락의 범위를 크게 가지면 뒤의 트랜잭션들이 기다리는 시간이 길어져 성능을 떨어뜨릴 수 있으니 최소화해야한다.

MySQL에서는 트랜잭션의 커밋 혹은 롤백시점에 잠금이 풀리는데 트랜잭션이 락의 범위가 되기 때문이다. 그래서 트랜잭션을 최소화하는 것이 락의 범위를 최소화하는 것이 된다.

읽기락(Shared Lock)과 쓰기락(Exclusive Lock)

읽기락쓰기락
읽기락(Shared Lock)O대기
쓰기락(Exclusive Lock)대기대기

읽기락끼리는 잠금을 서로 공유한다.

하지만 위와 같으면 모든 읽기를 하든 쓰기를 하든 잠금이 발생하는 데 MySQL에서는 일반 SELECT를 처리할 경우 nonblocking consistent read로 동작하여 대기하지 않고 처리가 가능하다. 참고

비관적(Pessimistic Lock) 락과 낙관적(Optimistic Lock) 락

위에서 알아본 요청을 락을 통해서 줄세우는 방식을 비관적 락이라고 한다. 동시성에 발생할거라 생각하고 방지하여 미리 줄을 세운다. 하지만 락을 통한 동시성 제어는 불필요한 대기 상태를 만들고 mysql에서는 인덱스를 잠그기 때문에 조건에 따라서 불필요한 잠김이 발생한다.

이에 반해 동시성 이슈가 빈번하지 않길 기대하고, 어플리케이션에서 제어하는 것이 낙관적 락이다.
CAS(compare and set)을 통해 제어하는 데 업데이트를 하기 전에 비교를 하고 맞으면 하고 맞지 않으면 하지 않는다.

낙관적 락의 흐름을 좋아요 기능으로 설명을 하자면
트랜젝션1이 좋아요 갯수 2와 버전 1을 조회하고 좋아요 갯수 3, 버전 2로 업데이트 치기 전에
트랜젝션2가 좋아요 갯수 2와 버전 1을 조회했을때 트랜잭션1은 조회한 버전 1에 좋아요 갯수 3, 버전 2로 업데이트를 완료했다.

이어서 트랜젝션2가 버전 1에 좋아요 갯수 3, 버전 2로 업데이트 하려하면 이미 버전은 2로 업데이트 되었기 때문에 정보가 맞지 않아서 업데이트에 실패한다. 실패에 대한 처리를 직접 구현 해야한다.

profile
사부작 사부작

0개의 댓글