목표
MySQL의 동시성 처리를 위한 Lock에 대해서 알아보겠습니다.
개요
최근 프로젝트를 진행하면서 배포를 앞에 두고 가장 큰 관심사는 동시성 처리입니다.
사용자가 늘어날수록 동시 요청이 많아질 것입니다. 현재 백엔드는 Spring boot로 구성되어 요청마다 스레드가 할당됩니다.
이 말은 즉, 여러 스레드가 한 DB에 동시에 요청을 보낸다는 뜻입니다.
DB에 여러 요청에 동시에 들어왔을 때, 데이터 정합성을 보장함과 동시에 성능에 영향을 주지 않도록 하는 방법에 대해서 알아보려고 합니다.
가장 먼저, 데이터 정합성을 보장하기 위한 Lock에 대해서 알아보겠습니다.
Lock의 설정 범위
Lock(락)은 여러 트랜잭션이 동시에 처리될 때 데이터의 무결성을 보장하기 위한 수단입니다.
한 트랜잭션이 처리되고 있을 때, 다음 트랜잭션이 잠시 기다렸다가 처리해 순차적으로 처리합니다.
락의 설정 범위(Level)
락을 설정하면 다른 트랜잭션이 접근할 수 없습니다. 불필요하게 많은 락을 설정한다면, 안전성은 올라갈지 몰라도 그만큼 성능이 저하됩니다.
이를 잠금 비용이라고 합니다. 잠금 비용이 올라갈수록 동시성을 처리하는데 성능 손실이 발생하게 됩니다.
따라서, 락의 설정 범위를 지정할 수 있고, 한 행(Row)부터 데이터베이스 전체까지 걸어둘 수 있습니다.
행(Row)
1개의 행을 기준으로 락을 설정합니다. Update, Insert와 같은 DML에 대한 일반적인 락입니다.
두 사용자가 한 행에 대해 동시에 요청을 했을 때, 앞선 사용자가 처리하는 동안에 다음 사용자는 해당 행에 접근할 수 없습니다.
열(Column)
1개의 열을 기준으로 락을 설정할 수 있습니다.
컬럼에 대한 접근을 막는 것인데, 설정과 해제에 대한 리소스가 많이 들어가 사용할 때 신중해야 합니다.
페이지, 블럭
일반적으로 데이터베이스는 파일의 형태로 저장됩니다. 해당 파일의 일부인 페이지나 블럭을 단위로 락을 설정할 수 있습니다.
DBMS에 의해 논리적인 저장 구조가 관리되는 만큼, 페이지나 블럭에 락을 거는 것은 많은 비용을 초래합니다.
많이 사용되지 않는다고 하지만, 저장되는 형태와 메모리에서 지역성의 원칙을 생각해 보면 활용될 여지도 있는 것 같습니다.
테이블
한 테이블에 대한 락을 설정할 수 있습니다. 전체 테이블을 수정하는 등의 작업을 할 때 유용하게 활용할 수 있습니다.
혹은, 테이블의 구조를 바꾸는 DDL 구문을 사용할 때 함께 사용할 수 있습니다. 이러한 성질로 인해 DDL LOCK이라고도 불립니다.
파일
페이지, 블록과 다르게 파일 전체에 락을 설정하는 것입니다.
스키마와 같은 메타 데이터를 갖고 있는 파일, 실제 데이터를 갖는 파일 등 각각의 파일로 저장됩니다.
메타 데이터를 포함한 파일을 수정하기 위해 락을 설정한다면, 실제 데이터의 스키마 등 잘못된 형태로 저장될 여지가 있습니다.
이러한 파일의 특성 때문에, 파일만 락을 설정하는 경우는 많지 않습니다.
데이터 베이스
전체 데이터 베이스에 락을 설정하는 것입니다. 단 한 개의 트랜잭션만 접근하는 것입니다.
DB의 버전을 업데이트하는 등 전체 DB에 대한 작업이 필요할 때 사용합니다.
Lock의 종류
락은 기능과 용도에 따라서 다양한 종류로 분류됩니다.
우선, 데이터 접근에 대한 락은 크게 공유 락과 배타 락으로 구분할 수 있습니다.
아래에서 말하는 객체란, 위 설정 범위에 따른 대상(행, 테이블 등)입니다.
공유 락(Shared Lock, Lock-S)
공유 락은 다른 트랜잭션이 잠긴 객체를 읽고 새로운 공유 락을 얻을 수 있지만, 쓰기는 불가능합니다.
예를 들어, A가 1행에 대해 공유 락을 걸었다면, B는 1행을 읽을 수 있지만, 수정은 불가능합니다. 이때, B는 새로운 공유 락을 걸 수 있습니다.
즉, 여러 명이 동시에 데이터를 읽기만 한다면 문제가 발생하지 않기 때문에 공유 락끼리는 동시에 접근이 가능한 것입니다.
읽는 도중 데이터의 변경이 있는 경우를 막는 것입니다.
주로 데이터를 읽는 데 사용하기 때문에 Read Lock(읽기 잠금)으로 불리고, Lock-S와 같이 표기합니다.
배타 락(Exclusive Lock, Lock-X)
배타 락은 다른 트랜잭션이 접근하는 것을 막는 락입니다. 데이터를 쓰는 경우(Insert, Update)에 사용하는 락입니다.
다른 트랜잭션이 접근해서 배타 락을 생성할 수 없기 때문에, 다른 트랜잭션의 데이터 쓰기가 불가능한 것입니다.
공유 락과는 다르게, 다른 락과 동시에 사용할 수 없습니다. (MySQL 문서에서는 반대되는 것이라고 표현하고 있습니다.)
다만, 무조건 배타 락이 걸려 있다고 데이터를 읽을 수 없는 것은 아니고, 락을 사용하지 않는 조회는 가능합니다.
예를 들어, 단순 SELECT 구절을 사용하면 배타 락이 걸려 있어도 조회할 수 있지만, SELECT ... FOR UPDATE 문을 사용해 락을 얻으면 배타 락이 걸려있는 데이터를 조회할 수 없습니다.
무조건 이렇다는 것은 아니고 다음 포스팅에서 다룰 고립 수준에 따라 다릅니다.
위 예시는 MySQL InnoDB의 기본 고립 수준인 REPEATABLE READ의 예시로, 보다 동시성 처리를 원활하게 하기 위한 것입니다.
대게 데이터에 변화를 줄 때 사용하는 락으로 Write Lock(쓰기 잠금)으로 불리고, Lock-X와 같이 표기합니다.
공유 락과 배타 락의 큰 차이점은 호환성입니다. 공유 락은 데이터가 바뀌는 게 없기 때문에 동시에 여러 개의 공유 락이 사용될 수 있습니다.
반면, 배타 락은 객체에 한 개의 락만 사용될 수 없습니다. 만약 배타 락이 걸려있는 곳에 또 다른 배타 락을 걸려면 끝날 때까지 기다려야 합니다.
뿐만 아니라, 배타 락과 공유 락을 사용할 때 문제가 발생하지 않게 돕거나, 성능을 저하시키지 않기 위한 락 등 다양한 락이 존재합니다.
업데이트 락(Update Lock)
업데이트 락은 배타 락을 걸 때, 교착 상태(데드락)를 방지하기 위해 사용하는 락입니다.
다음과 같은 상황이 있다고 가정해 보겠습니다.
파란색과 초록색은 트랜잭션을 의미하고, 트랜잭션 내부에는 두 테이블에 대한 데이터 수정 쿼리를 포함하고 있습니다.
파란색은 A 테이블을 수정하고 B 테이블을 수정합니다. 반대로 초록색은 B 테이블을 수정하고 A 테이블을 수정합니다.
두 트랜잭션은 자신이 수정하는 각 테이블에 배타 락을 걸어놓습니다.
첫 테이블을 수정하고 다음 테이블을 수정하려 하지만, 배타 락이 걸려있어 대기합니다.
배타 락이 풀리는 조건은 서로 순환되고 있어, 영원히 풀리지 않은 락을 계속 기다립니다.
이러한 상황을 교착 상태(데드락)이라고 합니다. 데드락이 발생하지 않도록 하는 방법은 무엇이 있을까요?
- 다음 트랜잭션이 들어오기 전에 빠르게 작업을 끝내고 락을 풀고 나간다.
- 해당 트랜잭션이 수정해야 되는 테이블에 모두 락을 걸어 놓는다.
빨리 끝내고 나가는 게 제일 좋겠지만, 불가능한 상황도 분명 있을 것입니다.
수정해야 되는 테이블에 모두 배타 락을 걸면 좋겠지만, 공유 락을 갖고 접근하는 경우도 막기 때문에 성능 저하가 우려됩니다.
이때, 사용하는 것이 업데이트 락입니다. 내가 수정해야 되는 테이블에 업데이트 락을 걸게 되면, 배타 락을 얻는 것을 방지합니다.
마치, 좀 있다가 수정하려는 것을 알려주는 것과 같습니다.
물론 데드락에 대해 100% 방지할 수는 없습니다. 만약, 여러 개의 트랜잭션이 접근해서 공유 락을 얻었다고 가정해 보겠습니다.
공유 락을 배타 락으로 변환하는 상황이 발생하면, 공유 락이 존재해 배타 락으로 변환할 수 없습니다.
이러한 경우 다시 데드락에 빠질 수 있습니다.
Intention Lock
Intention Lock은 테이블을 대상으로 하는 락입니다. 테이블 내에 Row, 페이지와 같이 더 낮은 수준의 락을 걸지 알려주는 락입니다.
MySQL의 InnoDB의 경우 Table Lock, Row Lock과 같이 다양한 수준의 락이 공존하는 것을 지원합니다.
어떤 락을 함께 사용할지 사용자의 의도를 알려주기 위해 Intention lock을 사용합니다.
Intention Lock은 크게 2가지 종류로 나눌 수 있습니다.
- Intention Shared(
IS) Lock - Intention Exclusive(
IX) Lock
IS, IX는 테이블 내 특정 행(페이지)에 해당 락(공유, 배타)을 획득하려는 의도를 나타내는 것입니다.
IS, IX는 여러 트랜잭션의 동시 접근을 허용하고 있습니다. 하지만, 실제 공유 락 혹은 배타 락을 거는 시점에 동시 접근을 막게 됩니다.
이렇게 2단계로 나누는 이유는 락이 걸려있는 상황에서 다른 락이 걸리는 것을 사전에 예방하려고 하는 것입니다.
예를 들어, A가 특정 Row에 배타 락을 걸고 B가 테이블 전체에 대한 수정을 하려고 합니다.
당연히 B는 A가 끝날 때까지 기다려야 합니다. B는 모든 Row에 대해서 락을 걸 수 있는지 판단해야 하는데, 이는 상당히 비효율적입니다.
A가 배타 락을 거는 시점에, 테이블에 IX 락을 걸면, B는 모든 Row를 확인하지 않아도 락을 걸 수 있는지에 대한 여부를 확인할 수 있습니다.
반대로, 테이블에 락이 걸려있는 상태에서 특정 Row에 락을 거는 상황을 원천적으로 예방할 수 있습니다.
이 외에도, 인덱스 레코드에 락을 거는 Record Lock, 인덱스의 일정 범위에 락을 거는 Gap Lock 등 다양한 락이 존재합니다.
Lock Escalation
Intention Lock에서 봤듯이, 하위 객체에 락을 걸면 상위 객체에 Intention Lock이 걸리게 됩니다.
하위 객체에 락을 많이 걸면, 상위 객체에 락을 건 것과 다름이 없음에도 불구하고 수많은 Intention Lock이 생기게 됩니다.
Lock이 많아질수록, 관리에 따른 코스트(메모리, 속도)가 발생하기 마련입니다.
차라리 상위 객체에 락을 하나 걸어놓게 되면 더 효율적으로 사용할 수 있습니다.
따라서 DBMS는 최적화를 위해 상위 수준의 락으로 변환하는 Lock Excalation이라는 프로세스를 갖고 있습니다.
Lock을 고려한 트랜잭션 성능 관점
DBMS는 다양한 목적으로 Lock을 활용하고 있습니다.
동시성을 처리하는 측면에서 Lock의 활용을 극대화하려면, Lock 사이의 호환성에 대해 생각해 볼 필요가 있습니다.
다음은 호환성을 나타내는 표입니다.
Exclusive ( |
X | X | X | X |
Intent Exclusive ( |
X | O | X | O |
Shared ( |
X | X | O | O |
Intent Shared ( |
X | O | O | O |
기본적으로 Intention Lock 끼리는 호환이 가능합니다. 배타 락의 경우 다른 락과 호환이 되지 않습니다.
호환이 된다는 것은, 여러 트랜잭션이 같은 락을 갖고 객체에 접근할 수 있는지에 대한 여부입니다.
배타 락이 걸려있는 상황에서 다른 트랜잭션에서 락을 걸려면 배타 락이 풀릴 때까지 기다려야 합니다.
이렇게 기다리는 것을 블로킹(Blocking)이라고 합니다. 기다리는 시간만큼 성능에 악영향을 미칠 가능성이 높습니다. (Side Effect)
따라서, 트랜잭션의 단위를 줄이고 수행 시간을 최소화하면 불필요하게 대기하는 시간을 줄이는 것이 좋습니다.
쿼리를 튜닝해 쿼리의 수행시간을 단축시키는 것이 DB 관점에서는 최선일 것 같습니다.
혹은 고립 수준을 너무 높게 설정하는 것도 성능만 바라봤을 때는 악영향을 미칩니다.
하지만, 교착 상태(데드락)에 빠져버리면 어떡할까요? 무작정 기다릴 순 없으니, 얼마나 기다릴 것인지에 대한 값을 적절하게 설정해줘야 합니다.
MySQL 5.7 버전 이후 InnoDB는 50초로 설정이 되어 있습니다.
설정된 시간이 지나면, 교착 상태에 빠진 트랜잭션(이후에 온)을 롤백처리하게 됩니다.
대기 시간을 설정하는 것 이전에, 락을 적절히 사용하고 순서를 올바르게 하여 데드락에 빠지지 않는 것이 우선일 것 같습니다.
정리
이렇게 MySQL8(InnoDB)에서 동시성 처리를 위한 Lock에 대해서 알아보았습니다.
락을 올바르게 활용하면 여러 트랜잭션이 동시에 요청해도 효율적으로 처리할 수 있습니다.
이 외에도, 트랜잭션의 단위를 적절하게 나누고 수행되는 쿼리의 성능을 튜닝한다면 효과를 극대화할 수 있을 것입니다.
락의 유무에 따라 어떻게 동작하는지는 DB의 격리 수준에 따라 다릅니다.
다음 포스팅에서는 DB의 격리 수준에 대해 알아보도록 하겠습니다.
Reference
'개발 > MySQL' 카테고리의 다른 글
[MySQL] MySQL 동시성 처리(2) - 트랜잭션의 고립성 보장을 위한 격리 수준과 MVCC (0) | 2023.03.19 |
---|