Be-Developer

데이터 중심 아키텍처 : 7.트랜잭션

07. 트랜잭션

애매모호한 트랜잭션의 개념

ACID의 의미

  • Atomicity 원자성
    • 원자적 : 여러 쓰기 작업이 하나의 원자적인 트랜잭션으로 묶여있어, commit 될 수 없다면 abort 되어 원자적인 트랜잭션은 실행된 쓰기가 무시되어야 한다.
  • Consistency 일관성
    • 트랜잭션이 불변식이 유효한 db에서 시작하고, 트랜잭션에서 실행된 모든 쓰기가 유효성을 보존한다면 불변식이 항상 만족된다고 확신 할 수 있다.
    • 일관성은 애플리케이션의 속성이며, 데이터베이스는 보장할 수 없다.
  • Isolation 격리성 (직렬성)
    • db는 여러 트랜잭션이 동시에 실행되었더라도, 순차적으로 실행되었을때의 결과와 동일하도록 보장한다.
    • 한 트랜잭션이 여러번 쓴다면, 다른 트랜잭션은 그 내용을 전부 볼 수 있던지없던지 둘중 하나여야한다.

      readonly Transaction의 존재이유?

  • Durability 지속성
    • db가 죽더라도 트랜잭션에서 기록한 모든 데이터는 손실되지 않는다는 보장.
    • 트랜잭션 커밋됨을 보고하기전에, 쓰기나 복제가 완료될때까지 db는 기다려야한다.

      단일객체 연산과 다중 객체 연산

  • 단일객체 연산, 격리성 보장을 위해 compare-and-set이 사용되기도 한다.

    다중 객체 트랜잭션의 필요성

  • 오류와 어보트 처리 : 오류 복구는 애플리케이션에게 책임이 있다.

    spring multiple datasource, transaction 풀에서 의 트랜잭션

    1. @Transaction(propagate) 설정으로 여러 트랜잭션을 묶는다? X
    2. spring-data-commons 의 ChainedTransactionManager O
      • 트랜잭션을 순차 생성, 에러발생시 역순으로 롤백하는데, 커넥션을 미리 들고있는 이슈가 있을 수 있으므로 LazyConnectionDataSourceProxy를 사용할 수 있다.
    3. JTATransactionManager (JTA : Java Transaction Api)
      • dtasource를 jta 에서 등록하여 관리, 성능저하 검토 필요. 동작 러닝커브가 조금 있음.
      • spring 3.0부터는 자체 지원 com.atomikos.transactions-spring-boot3-starter
      • 출처 : 다중 datasource 처리

        완화된 격리 수준

  • 맹목적으로 도구에 의존하기 보다는 존재하는 동시성 문제의 종류를 잘 이해하고 방지하는 방법을 배울 필요가 있다.

    커밋 후 읽기

  • DB에서 읽을때 커밋된 데이터만 보게 된다. dirty read 방지
    • dirty read : 다른 트랜잭션에서 커밋되지않은 변경사항을 보는것.
  • DB에 쓸때 커밋된 데이터만 덮어쓰게 된다. dirty write 방지
    • dirty write : 직렬성이 보장되지않는것. 한개의 객체에 업데이트 시차가 있어서 먼저 업데이트된게 무시되는것.
  • 구현
    • 일반적으로 row 수준의 lock을 걸어 사용한다.
    • 읽기잠금은 비효율적이어서, 일반적으로는 read시에는 lock을 가져간 트랜잭션이 커밋하기전의 과거값을 읽는방식.

      스냅숏 격리와 반복 읽기

  • nonrepeatableRead, read skew : read_commited 이어도 여러 트랜잭션의 실행 간극 사이에서 조회된다면, 싱크가 안맞아보일 수 있다. 하지만 이건 일시적인 현상.
  • 스냅숏 격리 : 각 트랜잭션은 데이터베이스의 일관된 스냅숏으로부터 읽는다.

    스냅숏 격리 구현

  • 잠금없이 db가 여러 버전을 함께 유지한다 .(다중버전 동시성 케어 MVCC)
    • postgres 예에서는, row마다 변경이력을 가지고있다. transactionId,생성,삭제 등등
    • 스냅숏은 bree를 사용한다

      색인과 스냅숏 격리

      반복 읽기와 혼란스러운 이름

  • 오라클에서는 직렬성 mysql에서는 repeatable read라고 한다.

    갱신 손실 방지

  • 갱신 손실은 read-modify-write 주기사이에 발생할 수 있다. (ex_ JPA)

    원자적 쓰기 연산

  • 관계형 db에서 concurrency-safe하다.
  • 보통 객체에 lock을 거는 방식으로 구현되어있다.
    UPDATE table SET value = value + 1 WHERE key = 0;
    
  • mongoDB : json 문서의 일부를 지역적으로 변경하는 원자적 쓰기연산
  • redis : 우선순위큐
  • jpa 에서 version?

    명시적 잠금

    갱신 손실 자동 감지

  • 애플리케이션 레벨에서 갱신 손실을 발견하면 트랜잭션을 abort시키고 재시도를 강제화한다.
  • compare-and-set

    충돌 해소와 복제

  • 다중리더 db에서는, compare-and-set 을 사용할 수 없다.
    • 이때는 한값이 여러개로 분산되는것을 감안하고 애플리케이션코드나 데이터구조로 해소및 병합시켜줘야한다.
  • LWW 최종 쓰기 승리 충돌 해소방법이 갱신손실은 많이 일어나지만 대중적.

    쓰기 스큐(skew)와 팬텀

  • 두개의 트랜잭션이 같은 객체를 읽어서 그중 일부를 갱신할때 발생.
    • SELECT -> 조건 확인 -> 도메인 작업 -> 트랜잭션 이 플로우가 동시에 일어나 조건확인에 여러 요청이 부합되었고, 트랜잭션이후 전제조건이 깨졌을경우,
    • dirty read와 다른점은 두개의 객체 각각에 업데이트가 일어났고, 이로인해 불변식이 깨지는것.
  • 팬텀(phantom) :어떤 트랜잭션에서 실행한 쓰기가 다른 트랜잭션의 검색 질의결과를 바꾸는 효과
  • 자동으로 감지하려면 직렬성 격리가 필요하다.
  • 직렬성 격리가 되지 않는다면, 트랜잭션이 의존하는 로우를 lock 하는것이 차선책.

    충돌 구체화

  • 위처럼 lock할 데이터가 없는경우 (데이터가 없는게 전제조건인 경우), 락을 걸수있는 임의의 데이터를 만들어두자.
    • 단순 lock역할의 객체 혹은 회의실 예약 시스템에서 시간텀
  • 다른 대안이 없을때 최후의 수단으로 고려하자. 대부분 직렬성 격리가 더 선호된다.

    직렬성

  • 동시성 문제는 테스트하기 어렵고, 예측하기 어렵다.

    실제적인 직렬 실행

  • 한번에 트랜잭션 하나씩만 직렬로 단일스레드에서 실행하는것. 직렬성 격리를 획득하는 실용적인 방법.
  • 모든 트랜잭션은 작고 빨라야한다.
  • 빨라야하기때문에 active한 데이터가 메모리에 적재될 수 있는경우로 사용이 제한된다.
  • 쓰기 처리량이 단일 CPU애서 처리할수 있을정도로 낮지않으면, 파티셔닝을 고려

    트랜잭션을 stored procedure 안에 캡슐화하기

  • 애플리케이션에서 한 트랜잭션안에서 조회, 분기(if), 쓰기 작업을 모두 한다고 하면, 질의간의 시간이 낭비되고 오래걸림.
  • stored procedure: 조회-분기-쓰기 를 db로 보내서 한번의 연결안에서 처리되게한다.

    stored procedure의 장단점

  • db 성능에 부하를 줄 수 있다.
  • 범용언어가 사용되지않았다. -> 현재는 범용언어 지원됨.

    파티셔닝

  • 트랜잭션이 순차적으로 처리되면, cpu 코어속도로 제한되므로, 스케일아웃하여 데이터 파티셔닝.
  • 파티셔닝에서도 트랜잭션이 가능하긴 하지만, 오래걸림
  • 파티셔닝에서 작업성능은 키값에 많이 의존된다.

    2단계 잠금(2PL)

  • 쓰기 트랜잭션은 다른 읽기/쓰기 트랜잭션을 진행하지못하도록 막는다.
  • 트랜잭션이 시작할때 잠금획득, 끝날때 모든 잠금 해제하여 2단계 잠금.
  • 2단계 잠금 | lock 보유 \ 다른 트랜잭션 | 쓰기 | 읽기 | | —–| —–| —-| | 쓰기 | 대기 | 대기 | | 읽기 | 대기 | 대기 |

  • 스냅샷 격리 (일반 잠금)
    • 읽는쪽은 결코 쓰는쪽을 막지 않으며, 반대도 성립해야한다. | lock 보유 \ 다른 트랜잭션 | 쓰기 | 읽기 | | —–| —–| —-| | 쓰기 | 대기 | 이전버전 조회 | | 읽기 | x | 대기x |

구현

  • 공유모드 : 읽을때, 한객체에 여러 공유모드 잠금이 있을수있다.
  • 독점모드 : 쓸때, 한 객체에 한개의 독점잠금만 허용된다.

    성능

  • 교착상때에는 자동으로 감지하여 한개의 트랜잭션을 abort하는데, 이때 취소된 트랜잭션은 처음부터 실행되어야하므로 불필요한 반복이 일어난다.
  • 동시성이 줄어들어 트랜잭션 처리량이 크게 나빠진다.

    서술 잠금

  • 특정 객체 잠금이 아닌 조건에 해당하는 모든 객체를 잠그는것.
  • 직렬성 격리를 쓰는 db는 팬텀문제를 막아야하는데, 서술잠금이 해결할수있다. (한 트랜잭션이 다른 트랜잭션의 쿼리결과를 바꾸는 문제)

    색인 범위 잠금

  • 서술잠금에서 조건을 간략화하여 인덱스범위를 잠그는것.
  • REPEATABLE_READ이면 이 락을 사용하면 인덱스잠금이 이용되어서 데드락 발생가능. -> READ_COMMITED로 사용되어야함.

    직렬성 스냅숏 격리(SSI)

    비관적 동시성 제어 vs 낙관적 동시성 제어

spring에서의 트랜잭션

  • @Transaction은 동일한 db커넥션을 사용하도록 도와주는 역할. 글로벌 트랜잭션을 사용하지않으면 일반적으로 datasource 마다 따로동작함.
  • JDBC : 동일한 커넥션내에서 start transaction ~ CUD ~ commit 메세지를 코드실행시마다 날려줌
  • JPA : spring app 내 영속성에서 쿼리를 들고있다가, 커밋시점에 커넥션잡고 한번에 날림.
  • @Lock 사용하지않으면 트랜잭션에서 row레벨 락을 걸지는 않는다.