Skip to content

silberbullet/high-traffic-concurrency-control-exploration

Repository files navigation

MSA를 이해하기 위해 대용량 트래픽과 동시성 제어 탐구

배경

2022년 이후 MSA 아키텍처의 변화 양상이 커지고 있다. 차세대 프로젝트 붐이 일어난 이후, 또 한번의 대규모 트래픽에 대응하기 위해 무거운 모놀리식 서버 안에 마이크로 서버로 분리를 할 수 있는 서비스를 분리하기 시작했다.

대용량 트래픽에서 발생하는 동시성 문제는 경험 해보았다. 차세데 프로젝트에서 등록 서비스를 구현 할 때, Key값을 가져오기 위해 MyBatis의 를 이용했었다. 아무런 제어도 없는 상태에서는 무결성 에러에 번번히 애를 먹고 지향하기로 했었다. (auto incrementKey generation 로직을 따로 빼는걸 추천 )

비즈니스 계층에서 나는 어떻게 대용량 트래픽을 최대한 대응할 수 있을까? 대용량 트래픽에 대응하기 위해 Spring Cloud의 로드밸런서, Redis의 캐시 기능등이 있는데 주니어 개발자는 아키텍처를 직접 고안하는 경험이 없으니 있는 자리에서 최대한 생각해보자.

목표

다중 트랜잭셕에서 발생하는 동시성을 먼저 이해하고, 해결방안을 최대한 생각해보자
DAO 계층에 쓰는 경우가 JPA 혹은 MyBatis 일때로 경우의 수를 나누어 생각해보자
Redis를 집중 학습하여 뭔가 해내자


목차

  1. 분석
  2. 설계

▶ 분석

  1. 동시성 제어 이해

    동시성 제어란?

    동시에 실행되는 여러 개의 트랜잭션이 작업을 성공적으로 마칠 수 있도록 트랜잭셕의 실행 순서를 제어하는 기법`

    graph LR
    A[트랜잭션 1] --> B[동시성 제어]
    A2[트랜잭션 2] --> B
    A3[트랜잭션 3] --> B
    
    subgraph 직렬화 수행
        direction LR
        B --> C[트랜잭션 3]
        C --> D[트랜잭션 2]
        D --> E[트랜잭션 1]
    end
    
    E --> F[DataBase]
    
    style B fill:#f9f9f9,stroke:#333,stroke-width:2px
    style C fill:#d9e8f6
    style D fill:#d9e8f6
    style E fill:#d9e8f6
    style F fill:#f8c6a0
    
    Loading

비즈니스에서 동시성 제어는 데이터의 무결성 및 일관성을 보장가 가장 중요하다.

동시성 제어 기법 종류?

1. 락킹 : 트랜잭션이 데이터에 잠금 lock을 설정하면 다른 트랜잭션은 해당 데이터에 대해 접근이 불가/ unlock시에는 접근/수정/삭제가 가능
2. 타임스탬프 : 생성하는 고유 번호임 타임스탬프을 트랜잭션에 부여함으로 트랜잭션간의 접근 순서를 미리 정할 수 있다.
3. 적합성 검증 : 먼저 트랜잭션을 수행하고 트랜잭션을 종료 할때 적합성을 검증하여 db 최종 반영

이론적으로 3가지가 존재하며 이를 코딩을 어떻게 실현 할 수 있을까?

  1. 동시성 제어 실현

    1. @Transactional isolation 활용

      @Transactional 이란?
      스프링에서 트랜잭션 처리를 위해 선언적으로 트랜잭션에 행위를 정의하게 해주는 프록시 객체라고 생각하면된다.
      
      동작 원리가 결구 AOP를 통해 구현되어 있기 때문에 내가 만든 로직이 @Transactional을 붙여주면,
      [ 트랜잭션 시작 - 내가 만든 로직 - 트랜잭션 종료 ] 으로 실행하게 된다.
      
      클래스, 메소드, 인터페이스 메소드 단위로 정의할 수 있으며
      
      [클래스 메소드-> 클래스 -> 인터페이스 메소드 -> 인터페이스] 우선순위가 존재한다.
      
      Spring에서는 클래스에 적용하는 것을 권고한다. 자바 어노테이션은 인터페이스로부터 상속되지 않기 때문에 클래스 기반 프록시에서 트랜잭션 설정을 인식할 수 없다.
      

      @Transactional 의 설정 중 isolation 이 있다. isolation은 일련의 트랜잭셕을 작업 중 다른 트랜잭션의 연산 작업이 끼어들지 못하도록 격리 시키는 것이다.

      isolation에는 한 트랜잭션이 조회, 변경중인 데이터에 대한 접근의 혀용 수준을 결정하는 격리 수준이 존재한다.

      1. Read-Uncommited (level 1) : 다른 Transaction에서 변경이 커밋되지 않았더라도 변경된 데이터를 조회할 수 있다.

      2. Read-commited (level 2): 커밋된 내용만 반영된 데이터에 대해서만 조회할 수 있는 격리 수준이다. 트랜잭션 도중 다른 트랜잭션에서 변경이 커밋되었다면 변경된 데이터를 조회할 수 있다. (Oracle DBMS 기본 수준, 온라에서는 많이 사용됨)

      3. Repatable-Read (level 3) : 트랜잭션이 시작되기 전에 커밋된 내용에 대해서만 조회할 수 있는 격리수준이다. 트랜잭션 내에서 조회하는 데이터에 대해 공유락을 걸어 잠금된 데이터의 변경 불가능이 보장된다. 공유락은 조회 된 한개의 레코드 단위로 잠금을 처리한다.(리소스의 WRITE 제한을 건다.) (MySQL DBMS 기본 수준)

        • 모든 InnoDB의 트랜잭션은 순차적으로 증가하는 트랜잭션 번호를 가지고 있다. 해당 격리수준은 자신의 트랜잭션 번호보다 낮은 트랜잭션 번호에서는 변경된(커밋된) 것만 보게 되는 것이다.
        • Update 부정합이 일어날 수 있다.
        • 새로 추가, 삭제 된 행에 대해서는 일관성을 보장하지 않기 때문에 Phantom Read가 발생
      4. Serializable (level 4) :트랜잭션 격리 수준 중 가장 높은 수준으로, 트랜잭션 간의 일관성과 무결성을 최대한 보장한다. 동시처리 능력이 다른 격리수준보다 떨어지고, 성능저하가 발생한다. 단 트랜잭션 내에서의 Select된 자원들은 공유 잠금된다.

      격리 수준은 높을 수록 성능은 떨어진다. 일반적인 웹 서비스는 level 2 지향하지만, 금전적인 예민한 데이터는 격리 수준을 높여야 한다. 즉, 동시성이 발생하더라도 비즈니스에 따라 제어 방향이 달라야 한다는 것이다.

      MySQL과 Oracle 아무거나 쓰면 되는거 아니야?

      MySQL은 OLTP(실시간 다수의 트랜잭션 처리 시스템)에 특화 되어 있는 DBMS이다. REPEATABLE READ 격리 수준을 사용하고 높은 동시성을 유지를 보장한다. 트랜잭션이 시작될 때의 데이터 스냅샷을 사용하여, 다른 트랜잭션이 데이터를 수정해도 현재 트랜잭션은 영향을 받지 않는다.

      Oracle은 읽기 작업이 최신 데이터를 요구하는 시스템에서 유용하다. 커밋된 데이터를 읽기 때문에 트랜잭션 간의 충돌이 적고, 높은 성능을 요구한다. 격리 수준도 한 단계 높은 Read-commited 쓰기 때문이다.

      결론은 시스템 혹은 목적에 맞는 서버 마다 RDBMS를 선택하는 것도 중요하다. 데이터를 관리, 이력을 가지는 업무는 Oracle이 유용할 거고, 많은 사용자가 동시에 시스템에 접근하더라도 신뢰성을 잃어버리면 안되는 업무는 MySQL이 적합하다.

    2. JPA @Lock, @Version 활용

      1. JPA 낙관적 락(Optimistic Lock)

        낙관적 락은 동시성 문제가 발생하지 않을 것이라고 가정하는 낙관적인 방법이다. 동시성 제어에 timestamp 기법을 응용한 방법이라고 생각한다.

        데이터를 읽을 때는 락을 걸지 않고, 업데이트 할 때만 이전 데이터와 현재 데이터의 Version을 검사하여 중돌이 발생하는지 확인하다. 충돌이 발생 시 OptimisticLockException 을 발생한다

         - 조회 시에는 버전을 확인하지 않기 때문에, 조회가 위주인 경우에는 충돌이 일어날 가능성이 적기에 적합하다.
         - 자주 업데이트가 일어나는 곳에서는 충돌이 발생할 가능성이 높아 적합하지 않다. 매번 exception을 잡아 처리해주게 되면 리소스가 커지기 때문이다.
         - 낙관적 락의 경우는 버전에 의한 겸사를 사용하기에 어플리케이션 단계에서 처리한다.
        
      2. JPA 비관적 락 (Pessimistic Lock)

        비관적 락은 동시성 문제가 발생할 것이라고 가정하는 비관적인 방법이다. 비관적 란은 트랜잭션이 시작될 때 공유 락 혹은 배타 락을 걸 수 있다.

        배타 락은 A 트랜잭션이 데이터를 읽고 있을 때 아예 락을 걸어 B 트랜잭션이 A 트랜잭션 작업이 끝날 때까지 대기 후 처리 하는 것이다.

        - @Lock(LockModeType.PESSIMISTIC_READ) : 공유 락을 의미한다.
        - @Lock(LockModeType.PESSIMISTIC_WRITE) : 베타 락을 의미한다.
        
      3. 격리 수준의 목적

    3. 적절한 격리 수준과 락

      격리 수준은 트랜잭션이 다른 트랜잭션의 중간 결과에 얼마나 접근 할 수 있는지에 대해 격리가 목적을 둔다. 은 데이터에 대한 접근 자체에 목적을 둔다.

      격리 수준을 높인다고 해서 동시성 제어가 가능할까? 성능도 챙길 수 있다면 어떻게 고민을 해야 할까?

▶ 설계

실시간으로 동시성이 자주 발생하는 두 가지 예제가 있다. 주문이체를 가지고 동시성 제어를 설계 해보자.

  1. 주문

    A 제품이 폭탄 세일을 시작했다. 주문 서버는 A 상품을 재고를 조회하여 결제 처리를 진행해야 한다. 폭탄 세일로 인해 동시 접속자가 증가하고 주문도 동시에 일어졌다. 적절한 격리 수준Lock을 걸어 제일 좋은 판단을 해보자.

    상품 테이블은 간단하게 상품코드, 상품명, 상품 재고, 상품 가격으로 구성한다.

    상품 주문 성공 시, 주문 성공 이력에 쌓아가는 방식으로 둔다.

    erDiagram
        SLITM_CD {
            VARCHAR(Key) SLITM_CD
            VARCHAR SLITM_NM
            NUMBER SLITM_STK
            NUMBER SLITM_PRC
        }
    
        ORD_SLITM_HIS {
            VARCHAR(Key) ORD_NUM
            VARCHAR(Key) SLITM_CD
            NUMBER SLITM_CNT
            NUMBER SLITM_PRC
            DATE ORD_SUC_DATE
        }
    
    Loading
    1. Serializable + 배타 락 VS Repatable-Read + Version

About

MSA를 이해하기 위해 대용량 트래픽과 동시성 제어 탐구

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages