동시성 이슈 - 들어가기 전에

more·2023년 8월 31일

동시성이슈

목록 보기
2/4

동시성 이슈 해결과 Lock에 대해서 공부하려면 Transaction 격리 수준에 대한 이해와 Lock이 무엇인지부터 알아야한다.

Transaction

Mysql 읽기 모드 (Mysql에서 데이터를 읽는 방법)

  • Consistent Reads
    • read(=SELECT) operation을 수행 할 때 현재 DB의 값이 아닌 특정 시점의 DB snapshot을 읽어오는 것
    • 다른 트랜잭션이나 작업에서 변경한 내용이 아닌, 현재 트랜잭션이 시작한 시점의 데이터를 읽는 것
    • 락을 사용하지 않아도 되며, 이는 동시성을 높이고 읽기 성능을 향상
      → 복구 비용은 발생하지만 동시성은 좋음

  • Locking Reads
    • 트랜잭션의 시점(read view)을 무시하고, 최신의 Commit된 데이터를 읽는다.
    • 해당 데이터에 읽기 락(Shared Lock)을 걸어 다른 트랜잭션에서의 변경을 막는 것을 의미
    • 데이터의 일관성보다는 데이터 변경의 격리를 중요시하는 경우에 사용

Transaction Isolation Level

  • READ UNCOMMITTED

    • SELECT 쿼리를 실행할 때 아직 commit 되지 않은 데이터를 읽어올 수 있다

    • 예를 들어

      1. Transaction A에서 row를 삽입했다.

      2. READ UNCOMMITTED level의 transaction B가 해당 row를 읽는다.

      3. Transaction A가 rollback 된다.

        이 경우에 transaction B는 실제로 DB에 한 번도 commit 되지 않은, 존재하지 않는 데이터를 읽음 → dirty read

    • InnoDB 엔진 (Mysql 5.5 버전 이상에서 적용, 이전에는 MyISAM) 은 일단 실행된 모든 쿼리를 DB에 적용. 아직 commit 되지 않았어도 적용.

    • 즉, 특별히 log를 보고 특정 시점의 snapshot을 복구하는 consistent read를 하지 않고 그냥 해당 시점의 DB를 읽으면 dirty read

  • READ COMMITTED (Oracle의 Default)

    • read operation 마다 DB snapshot을 다시 뜬다.
    • commit 된 데이터만 보이는 수준의 isolation을 보장
    • 실제 DB에는 아직 commit 되지 않은 쿼리도 적용된 상태 (Mysql). 따라서 commit 된 데이터 만을 읽어오기 위해서는 아직 commit 되지 않은 쿼리를 복구하는 과정이 필요. 즉, consistent read를 수행해야 한다
    • record lock만 사용하고 gap lock은 사용하지 않는다 ⇒ phantom read가 일어날 수 있다.
    • 예를 들면, 테이블 t에 c = 13과 c = 17이 있고, A가 read commited level로 c1 컬럼을 10~20까지 읽어드리는 과정에서 B가 repeatable read level로 c1 컬럼에 15를 업데이트 하면 A는 원래 15가 없어서 13과 17만 읽어드려야 하지만 A commit 전에 B의 insert (아직 commit은 안되었음) 가 실행되면 A가 15도 읽어드릴 수 있음
    • gap lock을 사용 안 했기 때문에 13과 17을 제외한 10~12, 14~16, 18~20은 lock이 걸려있지 않기 때문이다.
  • REPEATABLE READ (Mysql에서의 Default)

    • 반복해서 read operation을 수행하더라도 읽어 들이는 값이 변화하지 않는 정도의 isolation을 보장

    • 처음으로 read(SELECT) operation을 수행한 시간을 기록

    • 이후에는 모든 read operation마다 해당 시점을 기준으로 consistent read를 수행

    • transaction 도중 다른 transaction이 commit 되더라도 새로 commit 된 데이터는 보이지 않는다.
      → 다른 사용자가 commit 해도 그 바뀐 값이 이미 읽은 사용자한테는 변화한 것으로 보이지 않고 기존 값으로 보임

    • 보통 gap lock을 활용, 조작을 가하려고 하는 row의 후보군을 다른 transaction이 건들지 못하도록 한다

  • SERIALIZABLE

    • 기본적으로 REPEATABLE READ와 동일.
    • 대신, SELECT 쿼리가 전부 SELECT ... FOR SHARE로 자동으로 변경 (Shared Lock을 거는 것)
    • S lock이 자동적으로 적용되기 때문에 transaction A와 B가 동시에 id = 1인 부분을 select 하면 id = 1인 부분에 S lock이 걸리기 때문에 A나 B가 해당 row를 업데이트하려고 해도 DEAD LOCK 상태에 빠져서 업데이트 할 수 없다.

Transaction 격리 레벨 변경만으로 동시성을 제어하기 어려운 이유

  1. 분산 트랜잭션 관리의 복잡성
  • 다중 서버 환경에서는 여러 서버 간의 트랜잭션 관리가 필요, 이로 인해 트랜잭션의 일관성과 격리를 유지하기 어려움
  1. 데이터 동기화 어려움
  • 여러 서버에 데이터를 동기화해야 하며, 데이터의 불일치 및 충돌을 피하기 위한 메커니즘이 필요
  1. 락 및 동기화 관리의 어려움
  • 다중 서버에서 데이터 접근을 동기화하려면 복잡한 락 관리가 필요, 이로 인해 락 경합과 성능 저하가 발생
  1. 트랜잭션 격리 수준의 제한
  • 격리 수준을 높이면 동시성이 감소하고, 격리 수준을 낮추면 데이터 일관성 문제가 발생.
  • 격리 수준 변경만으로는 이러한 균형을 완벽하게 유지할 수 없음
  1. 분산 시스템 관리의 복잡성
  • 서버 추가 및 제거, 장애 복구 등을 관리하는 것은 복잡하며, 이로 인해 시스템 안정성에 영향

=> 동시성을 제어하기 위해서는 Transactional isolation level을 변경하는 것 만으로는 어려움이 있음

Synchronized

Transaction 격리 수준으로 동시성 제어가 힘들다면 Synchronized를 사용하면 안되나?

  • Synchronized
    • 해당 키워드는 한 개의 스레드만 접근이 가능하도록 해준다.
      -> Application 수준에서 제공
    • synchronized는 각 인스턴스안에서만 thread-safe가 보장
    • 다중 서버일땐 레이스 컨디션이 발생
    • 최근 서비스는 다중 서버를 사용하기 때문에 synchronized는 거의 사용되지 않음.

Lock

트랜잭션 처리의 순차성을 보장하기 위한 방법이다.
멀티 트랜잭션 환경에서 여러 요청이 들어올 시 데이터베이스의 일관성과 무결성을 유지하기 위해서 트랜잭션의 순차적인 진행을 보장하기 위해서는 Lock이 필요하다.

Lock의 종류

  • Row-level lock

    • 테이블의 row마다 걸리는 row-level lock으로 가장 기본적인 lock 방식
    • Shared Lock (공유 잠금)
      • 데이터를 읽는 것을 잠금시키는 락
      • Shared Lock 끼리는 동시에 접속이 가능 -> 동시에 하나의 데이터를 읽는 것은 가능
      • 데이터 변경은 동시에 할 수 없다. -> Shared Lock이 걸리면 Exclusive Lock을 걸 수 없음

    • Exclusive Lock (배타적 잠금)
      • 데이터를 변경시키고자 할 때 사용하는 락
      • update, delete, select ... for update (배타적 잠금을 거는 것) 등의 기능을 잠금
      • 해당 락이 걸린 테이블 혹은 row의 데이터에 함께 Lock을 걸 수 없음
        -> 동시에 하나의 데이터 수정 불가능

    • 사용자 A가
    (Query 1 in transaction A)
    SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE;

    라는 쿼리를 실행 → t.c1의 값이 10인 index에 X lock이 걸림

    이 때 다른 사용자 (or transaction) 가

    (Query 2 in transaction B)
    DELETE FROM t WHERE c1 = 10;

    이런 쿼리 (c1 = 10을 삭제) 를 실행하려고 하면 사용자 A가 transaction을 commit 하거나 rollback 하기 전까지 (transaction이 끝나기전까지) 는 삭제할 수 없다.

  • Gap lock

    • DB index record의 gap에 걸리는 lock (gap이란 index 중 DB에 실제 record가 없는 부분)

    • 조건에 해당하는 새로운 row가 추가되는 것을 방지

    • 예를 들어 c1이라는 컬럼이 있는 테이블 t에는 c = 13, c= 17 이라는 두 컬럼이 있다하면

      (Query 1 in transaction A)

      SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE;

      10 <= id <= 1214 <= id <= 1618 <= id <= 20에 해당하는 gap에 lock이 걸린다.

      따라서 c = 15인 부분을 insert 하고자 해도 삽입이 되지 않는다.

Lock 설정 범위

  • 데이터베이스 : 주요 데이터베이스 업데이트시 사용
  • 테이블 : 전체 테이블에 영향을 주는 변경시에 사용 (DDL Lock이라고 함)
  • Row (행) : 행 기준으로 Lock 설정
  • 그 외 : 파일, 페이지, 블록, 컬럼 -> 잘 사용되지 않음

Lock 실행 시 Transaction 작업 대기 (Blocking)

  • Lock간의 경합이 발생하여 특정 Transaction이 작업을 진행하지 못하고 멈춰선 상태
  • Shared Lock 끼리의 상황에서는 발생하지 않음
  • A가 commit 하기 전에 B가 해당 데이터를 select 한다면, A가 실행한 행동 (update나 delete) 전의 데이터를 사용하기 때문에 문제가 발생할 수 있음
  • 따라서 A가 commit 혹은 rollback 하기 전까지 B는 해당 데이터에 접근 할 수 없음
    => 데이터의 일관성, 무결성은 유지가 되나 성능에는 좋지 않음

Dead Lock (교착 상태)

  • 두 트랜잭션이 각각 Lock을 설정하고 다음 서로의 Lock에 접근하여 값을 얻어오려고 할 때 이미 각각의 트랜잭션에 의해 Lock이 설정되어 있기 때문에 양쪽 트랜잭션 모두 영원히 처리가 되지않게 되는 상태
  • 예를 들어, A가 'ㄱ' 테이블의 정보를 수정하려고 할 때에 'ㄴ' 테이블의 정보가 필요한 상황에서, B도 'ㄴ' 테이블을 수정하는 상황에 'ㄱ' 테이블의 정보가 필요하다면 서로 배타적 잠금이 걸려 있기 때문에 A는 'ㄴ' 테이블에, B는 'ㄱ' 테이블에 접근 할 수 없고, 서로의 락이 풀리기 만을 기다리는 상황이다.

참고

데이터베이스 락
공유 잠금과 배타적 잠금의 차이
재고시스템으로 알아보는 동시성이슈 해결 방법
Lock으로 이해하는 Transactional isolation level
InnoDB 읽기 수준
Transactional 격리 수준

0개의 댓글