ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 재고 정합성이 걱정되어서 써보는 글(MVCC)
    글또 2024. 8. 21. 23:36

    0. 들어가며

    안녕하세요.
    이번에는 회사에서 개발하고 있는 재고 시스템에서 고민했던 점을 써보려고 합니다.
    내용적으로는 MVCC와 lock에 대한 이야기입니다. 
    전체적인 개념이 아닌 제가 궁금했던 점만 기술하니 참고 바랍니다.

    이 글은 MySql 5.6의 기본설정인 innodb와 repeatable read를 전제로 합니다.

     

    1. 갑자기 궁금해졌다
    2. MVCC의 목적
    3. Update Where... / Delete Where... (Consistent Nonlocking Reads)
    4. 우리의 재고 정합성을 위해 무엇을 해야 하는가

     

     

    1. 갑자기 궁금해졌다.

    새로 개발하고 있는 재고 관리 시스템에서 여러 명이 동시에 재고를 이동시키면 문제가 없을까?

     
    문제가 없는 경우는 유저가 한 명이고 순차적으로 기능을 실행시켜야만 가능한 이야기입니다. 
    즉 lock을 잡지 않으면 재고에 문제가 생길 수 있습니다.
    예측할 수 있는 문제는 lost update입니다.
     
     

    LOST UPDATE

    먼저 lost update는 트랜잭션의 내용이 덮어 씌워져 update내용을 잃는다는 것입니다. 

    위와 같이 lock 없이 각자 스레드에서 재고를 업데이트한다면 트랜잭션 1의 내용은 잃게 되고 이것을 lost update라고 합니다.

     

    SELECT FOR UPDATE (Locking Read)

     

    이는 lock을 잡아서 해결할 수 있습니다.
    lock에도 여러 가지가 있지만 Exclusive lock을 기준으로 설명드리면 다음과 같습니다.

     

     

    결과가 동일하게 50이라도 이를 lost update로 볼 수는 없습니다. 이는 트랜잭션 2가 stock을 읽었을 때 트랜잭션 1의 수정 내역인 10을 읽었기 때문입니다. 따라서 이는 lost update가 아닙니다.

     

    Repeatable read 격리 레벨에서 Locking read(Select for update)예제를 보자면, 트랜잭션B는 Select 시 대기상태로 됩니다.

    만약 트랜잭션B가  Locking read가 아닌 기본 Select를 했고 Update를 했다면 lost update가 발생했을 것입니다.

    이를 방지하기 위해 재고를 변경시키는 읽기에서는 모두 Locking read를 사용해야합니다.

     

     

    근데 트랜잭션 번호는 어디서 봤었지?

    과거에 공부했을 때 트랜잭션에 번호가 있어서 이전 트랜잭션은 읽지 않는다는 것을 보았던 거 같습니다.
    이게 위의 문제를 lock 없이 해결할 수 있을까요?
    결론은 lost update 해결과는 상관이 없는 기술입니다.
    먼저 트랜잭션 번호 (TRX_ID)는 MVCC를 공부했을 때 보았던 것이었습니다. 
     
     

    2. MVCC의 목적

    공유 자원은 동시성 제어(concurrency control)의 문제가 있습니다. 
    동시에 여러 유저가 공유 자원에 접근하면 이를 효과적으로 컨트롤해야 하는데 locking을 통한 상호배제로 해결하면 필연적으로 성능(동시성)이 떨어지게 됩니다.
    그래서 동시성을 높이고 싶어 MVCC가 등장했습니다.  
    MVCC는 동시성을 높였지만 대표적인 lost update라는 문제가 발생할 수 있습니다.

    MVCC Lock


     

    MVCC의 구현방법

    MySql innodb의 기본 트랜잭션 격리 수준인 repeatable read는 다른 트랜잭션이 커밋한 내용을 보지 않는다는 것을 공부했었습니다.
    이는 MVCC 기술을 통해 consistent read를 구현했기 때문입니다.
    어떻게 구현했을까요.
     
    트랜잭션은 시작 후 처음 조회를 했을 때 snapshot을 생성합니다.
    외부 트랜잭션에서 update나 insert가 발생해도 snapshot을 기준으로 데이터를 읽기 때문에 외부 영향을 받지 않는 것입니다.
    데이터를 읽는 순서는 버퍼 풀에 있는 데이터를 먼저 보고 볼 수 없는 trx_id이면 undo log를 같은 규칙으로 읽어나갑니다.
     

    READ COMMITTED도 MVCC를 사용하지만 commit 될 때마다 snapshot이 새로 만들어집니다.
    undo log는 트랜잭션의 롤백을 위해 변경 전 데이터를 저장합니다.
    마지막으로 변경시킨 트랜잭션 id도 함께 저장하고 있습니다.

    https://www.alibabacloud.com/blog/an-in-depth-analysis-of-undo-logs-in-innodb_598966

    Read view(Snapshot)

    어떻게 undo log와 snapshot을 가지고 이전 데이터를 구분할까요?
    저는 이 부분이 가장 궁금했습니다.
     
    먼저 snapshot은 read view를 말합니다.
    테이블 데이터를 캡처하는 snapshot이 아니고 활성 트랜잭션들의 정보를 캡처합니다.
    read view는 아래와 같은 칼럼들이 있고 이것을 활용해 데이터를 읽습니다.
     

    생성 규칙: 트랜잭션 내부에서 처음 select 할 때를 기준으로 생성합니다.
    데이터 읽는 규칙

    • trx_id=creator_trx_id: 현재 트랜잭션에서 수정된 정보이므로 읽을 수 있습니다.
    • trx_id <up_limit_id:  과거에 커밋된 트랜잭션이므로 읽을 수 있습니다.
    • trx_id> low_limit_id: 현재 트랜잭션이 생성된 후에 생성된 정보이므로 읽을 수 없습니다.
    • up_limit_id < trx_id < low_limit_id: 버전이 중간일 때입니다. 이때 trx_ids 목록을 살펴봐야 합니다. 들어 있으면 활성 트랜잭션이므로 접근할 수 없고, 없으면 이는 커밋되었고 접근 가능하다는 의미입니다.

     
     

    격리 수준을 공부하다 보면 repatable read에 단점으로 phantom read가 발생할 수 있다는데?

    phantom read라는 게 무엇이냐면 트랜잭션 내부에서 여러 번 select 시 result의 개수가 변경되는 현상을 말합니다.
    왜 변경되냐면 트랜잭션 실행 중에 다른 트랜잭션이  그새 insert 해서 그 row까지 읽는 것입니다.
    일반적인 MVCC환경에서는 발생하지 않지만 locking read( Select for update..)를 했을 경우 발생할 수 있습니다.
    locking read는 undo log를 읽는 것이 아닌 테이블을 읽기 때문에 변경된 데이터를 읽는 것입니다.
     
    근데 mysql에서는 방어가 되어 있습니다.
    locking read 시 next key lock을 잡기 때문에 phantom read는 발생하지 않습니다. 다른 트랜잭션은 insert를 하지 못하고 대기 상태가 됩니다. 
     
    다만 처음은 locking read로 읽지 않고 나중에 locking read로 데이터를 읽는다면 phantom read가 발생할 텐데 이는 개발자가 발생하지 않도록 하는 것이 좋겠습니다.

    3. Update Where... / Delete Where... (Consistent Nonlocking Reads)

    Repeatable Read 격리 수준은 MVCC를 통해 일관된 읽기를 구현할 수 있습니다. 

    그러나 이는 SELECT 문에만 적용됩니다. 

    UPDATE와 DELETE 문은 undo 로그를 통해 데이터를 읽는 대신 테이블 스페이스에 직접 접근하므로, lock을 잡고 다른 트랜잭션의 커밋된 사항도 읽을 수 있습니다. 

    또한, UPDATE와 DELETE로 변경된 내역은 트랜잭션 내부에서 다시 읽을 수 있습니다.

    이는 장점이 될 수도 있지만 타 트랜잭션의 커밋 내역도 읽는다는 점에서 주의가 필요합니다.

    (이렇게 구현된 이유는 아마 데이터 일관성을 지키기 위함이 아닐까요?)

    만약 아래 예제에서 첫 번째 조회된 부분만을 업데이트하려 했다면, 첫 SELECT 문을 Locking Read로 실행했어야 합니다. 이렇게 했다면 WHERE 조건에 사용된 컬럼에 대해 Lock이 걸리며(사용된 인덱스 종류에 따라 Gap Lock까지 포함될 수 있음), 다른 트랜잭션에서 INSERT나 UPDATE가 불가능했을 것입니다.

     

    추가로, UPDATE나 DELETE 문이 실행된 이후에는, 다른 트랜잭션이 해당 조건문에 사용된 컬럼에 대해 잠금을 대기하게 됩니다.

    https://dev.mysql.com/doc/refman/8.4/en/innodb-consistent-read.html

     

     

    4. 우리의 재고 정합성을 위해 무엇을 해야 하는가

     

    먼저 저희의 상황을 살펴보아야 합니다.

    • 어드민 서비스는 사용자가 적고 충돌 위험이 적습니다. 
    • MySQL의 격리 수준이 Repeatable Read이기 때문에, 트랜잭션은 기본적으로 MVCC 기반으로 동작합니다.
    • 이로 인해, lost update 현상으로 재고가 틀어질 수 있습니다.

     

    Pessimistic lock

    저희는 재고는 일관성을 유지하기 위해 Pessimistic Lock을 사용하였습니다.

    재고 차감기능에서는 SELECT FOR UPDATE로 lock을 잡아 lost update를 예방했습니다.

    다른 lock 방법보다 SELECT FOR UPDATE를 사용한 이유는 어드민 서비스가 사용자 수가 적어 lock관리가 수월했고 재고는 동시성보다는 일관성을 우선시해야 하기 때문이었습니다.

     

    • Gap Lock으로 인해 발생할 수 있는 문제들에 대해서도 대처가 필요합니다.  Composite UK를 사용하고 있어서 Gap의 범위가 작고 Dead Lock 발생 시 사용자에게 재시도를 유도했습니다.  
     

    Lock에 대해 알아보기: Gap Lock

    0. 들어가며 안녕하세요. 오늘은 Gap Lock에 대해 알아보려고 합니다.Gap Lock에 관심을 갖게 된 이유는 재고 관련 Locking을 공부하다가 궁금한 점이 생겼기 때문입니다. 재고 정합성을 위해 SELECT FOR UP

    gisungcu.tistory.com

     


    +

    또한, MySQL 8.0부터 추가된 NOWAIT와 SKIP LOCKED 기능은 Locking Read와 함께 사용할 수 있을 것 같습니다.

    이 기능들은 잠금이 걸린 레코드를 건너뛰고 데이터를 읽는 데 유용합니다.

    적절한 인덱스 설계만 잘 되어 있다면 특정 레코드에만 잠금이 걸리므로 SKIP LOCKED가 불필요할 수 있지만, 다음과 같은 경우에 유용할 수 있습니다.

    예를 들어, 범위 검색이나 LIMIT을 활용한 조회, 혹은 다른 인덱스를 통해 데이터를 조회할 때 효과적으로 사용할 수 있습니다.

    하지만 현재 저희 회사는 MySQL 8.0 버전을 사용하지 않아, 직접 이 기능을 활용하지는 못하고 있습니다.

    .

     

     

    Optimistic lock

    출고 작업 시에는 알바 작업자분들로 인해 트래픽이 증가하는 시간대입니다. 

    이때는 서비스는 일관성과 동시성이 중요했습니다.

    우선, 재고 차감을 통해 재고를 분리하고, 출고 과정에서는 각 단계별로 상태를 부여했습니다.

     

    출고 프로세스의 상태 관리를 위해, UPDATE WHERE을 활용한 Optimistic lock을 적용했습니다.

    이 방식은 데이터 경합으로 인해 업데이트가 이뤄지지 않아도 다시 재시도를 통해 문제를 해결할 수 있습니다.

     

    일반적으로 Optimistic lock을 떠올리면 Version 컬럼만 생각하기 쉽지만,  엔터프라이즈 애플리케이션 아키텍처 패턴 16장에서 여러 컬럼을 조합한 Optimistic lock을 설명합니다.

    저희는 Version대신 상태로 관리했습니다.

    이를 통해 update 중에는 locking이 되기 때문에  lost update를 예방할 수 있었습니다.

     

     

     

     

     

    현재의 상황을 살펴보고 적절한 기술을 선택해야 합니다.

    Redis 등을 도입하여 관리 포인트가 증가하는 것보다, 상황에 맞는 최적의 솔루션을 도입하는 것이 중요하다고 생각했습니다.

     

     

     

    다른 lock 방법

     

    동시성 해결에 관해

    해당 글은 인프런 강의를 토대로 작성되었습니다. 재고시스템으로 알아보는 동시성이슈 해결방법 - 인프런 | 강의 동시성 이슈란 무엇인지 알아보고 처리하는 방법들을 학습합니다., - 강의 소

    gisungcu.tistory.com

     
     
     

     

    참고자료

     
     

    '글또' 카테고리의 다른 글

    Lock에 대해 알아보기: Gap Lock  (0) 2024.09.01
    Lock에 대해 알아보기: Gap Lock 2  (0) 2024.08.30
    Galera Cluster 알아보기  (0) 2024.06.19
    IP White List: 보안 취약점과 강화 방안  (0) 2024.06.19
    Spring Boot 3 마이그레이션  (0) 2024.04.14

    댓글

Designed by Tistory.