현재 진행하고 있는 뮤신사 프로젝트에서는 MariaDB를 사용하고 있고 InnoDB 스토리지 엔진을 쓰고 있습니다.
MariaDB의 InnoDB는 MVCC로 동시성 제어를 하고 있습니다.
프로젝트를 진행하며 사용하는 데이터베이스의 특성에 대해서도 제대로 알아야한다는 필요성을 느꼈기 때문에, 오늘은 동시성 제어와 MVCC의 개념에 대한 포스트를 작성해보려고 합니다.
동시성 제어란 DBMS가 다수의 사용자 사이에서 동시에 작용하는 여러 트랜잭션의 상호 간섭 작용에서 데이터를 보호하는 것을 의미합니다.
일반적으로 동시성을 허용하면 일관성이 낮아지게 됩니다.
동시성 제어에는 크게 낙관적 동시성 제어와 비관적 동시성 제어가 존재합니다.
비관적 동시성 제어는 값을 수정할 때(또는 수정을 위해 읽어올 때), DB에 직접 Lock을 걸게 됩니다.
DB Lock에는 공유락(Shared Lock, S-Lock)과 배타락(Exclusive Lock, X-Lock)이 존재합니다.
획득한 락을 해제하는 방법은 커밋과 롤백 밖에 없습니다.
이러한 비관적 동시성 제어에는 몇가지 문제점이 존재합니다.
이런 문제점들을 해결하기 위해 MVCC가 탄생하게 되었습니다.
MVCC는 다중 버전 동시성 제어라는 의미를 가지고 있습니다.
말 그대로, 여러 개의 버전을 만들어 놓고 관리하면서 데이터의 일관성을 맞추는 방법입니다.
A 트랜잭션이 어떤 데이터를 변경하려는 경우, 변경 전 데이터를 Snapshot으로 만들어 보관합니다.
A 트랜잭션이 커밋/롤백 되기 전에 B 트랜잭션이 같은 데이터에 접근하면 Snapshot에 저장된 값을 읽어갑니다.
A 트랜잭션이 커밋되면 디스크에 변경 값이 반영되고, 롤백된다면 Snapshot의 값으로 데이터를 복구합니다.
그리고 데이터를 변경하려고 할때, 자신이 읽은 버전 정보와 DB의 버전 정보를 확인하여 일치하지 않으면 변경하지 않습니다.
MVCC를 통해서, 다른 트랜잭션은 아직 커밋되기 전의 변경 데이터가 아닌 기존 데이터를 읽어갈 수 있고, 두 트랜잭션이 거의 같은 데이터를 변경하여 생기는 lost update의 문제를 막을 수 있게 됩니다. 업데이트 시에 언두 로그에 변경 전 데이터가 적히기 때문에, 어떤 트랜잭션이 먼저 업데이트했는지를 알 수 있습니다. 따라서 늦게 업데이트한 트랜잭션의 변경사항은 저장되지 않고 롤백되게 됩니다.
Mysql(MariaDB)의 InnoDB는 언두로그(Undo log)를 작성하여 스냅샷 기능을 제공합니다.
즉 데이터의 변경이 일어나면 언두로그에 변경 전 데이터를 작성해놓고, 버퍼 풀(buffer pool)에 변경된 데이터를 적은 후, 커밋이 되면 버퍼 풀에 적힌 변경 내역을 디스크로 내려주는 것입니다.
트랜잭션 A가 아래와 같은 쿼리를 날렸다면, MariaDB의 InnoDB에서는 어떤 일이 발생할까요? (아직 커밋되지 않았다고 가정)
UPDATE product SET product_name = '어쩌고저쩌고' WHERE product_id = 17;
간략화해서 그려보면 위와 같은 상황이 됩니다.
이런 상황에서 만약 트랜잭션 B가 해당 로우 데이터를 읽으려고 한다면, 어떤 값이 반환될까요?
이는 트랜잭션의 격리 레벨에 따라 달라집니다.
=> 격리 레벨에 대한 설명 보러가기
트랜잭션 격리 레벨에는 4가지가 존재하는데
"uncommitted read"에서는 아직 커밋되지 않은 트랜잭션의 변경 사항도 볼 수 있기 때문에, 버퍼 풀에 존재하는 데이터를 리턴하고, (사실 uncommitted read는 많은 데이터 부정합 문제 때문에 거의 쓰지 않는 격리 레벨입니다.)
"committed read", "repeatable read"에서는 언두 로그에 있는 값을 읽어오게 됩니다.
"serializable"에서는 A 트랜잭션이 끝날 때까지 B 트랜잭션은 대기를 하게 됩니다.
이 공부를 시작한 이유가 상품 주문 시, 다른 회원들이 동시에 같은 상품을 주문할 때 재고 감소 처리를 어떻게 구현할지 정하기 위함이었습니다.
제가 짠 주문 로직은 이렇습니다. (재고 감소 부분만 작성)
1. 재고(stock
)를 읽어옵니다.
2. stock - quantity
가 0이상인지 확인합니다. (음수이면 예외를 던집니다.)
3. 0 이상이라면 재고를 stock - quantity
로 업데이트 합니다.
사실 부끄럽게도, MariaDB의 innoDB 스토리지 엔진이 MVCC를 따르고 있다는 것을 몰랐습니다.
따라서 비관적 락으로 정합성 문제를 해결하려고 했고, SELECT ... FOR UPDATE
문으로 재고를 읽어오면 같은 커넥션에서 실행되는 트랜잭션이 끝날 때까지 해당 데이터에 X락이 걸리게 되고 UPDATE
문으로 재고를 수정한 뒤 커밋하는 방법을 생각했습니다.
MariaDB의 innoDB는 MVCC로 동작하고 있기 때문에, 같은 데이터를 수정하려고 하면 더 늦게 수정한 트랜잭션이 롤백하도록 되어있습니다. 따라서 굳이 SELECT ... FOR UPDATE
로 락을 걸어줄 필요가 없다는 것을 알게되었습니다. 주문 메소드 위에@Transactional
만 붙여준다면 여러 트랜잭션이 동일 로우를 업데이트하는 문제는 해결이 됩니다.
물론, 같은 데이터를 고쳤을 때 생기는 문제(아직 알아보지는 못했지만 아마도 Exception일텐데)는 애플리케이션에서 구현해서 처리해주어야 할 것입니다.
MariaDB 공식 문서를 통해 같은 로우를 업데이트 했을 때 어떤 Exception이 던져지는지를 찾아보아야 할 것 같습니다.