서비스 운영 중에 심심치 않게 발생하는 에러 중, DB Lock관련 에러가 있다.
백엔드 개발자 입장에서는 해결하기 꽤나 어려운 에러인데, DB 입장에서는 아주 중요한 기능 중에 하나이다.
이번 글에서는 어떤 경우에 DB Lock이 발생하고, 왜 DB Lock이 필요한지, 어떻게 DB Lock 에러를 피할 수 있는지 알아보자.
(다양한 DB 중, MySQL을 갖고 이야기하도록 하겠다.)
DB는 태생적으로 데이터의 무결성과 일관성이 반드시 유지되어야만 한다.
그렇게 하기 위해서 구현된 기능 중에 하나가 DB Locking 이다.
동일한 데이터에 대해서 동시에 여러 쿼리가 수행될 때 Lock이 발생하는데, 하나의 쿼리가 수행되는 동안 해당 데이터에 대해서 Lock을 걸어서, 다른 쿼리가 수행되지 못하도록 하는 것이다.
MySQL의 경우, InnoDB 엔진과 같은 Storage engine에서 Lock을 걸고, 관리하는 역할을 담당한다.
MySQL의 InnoDB 엔진에서 제공하는 Lock중에서 기본적인 Lock을 먼저 알아보자.
특정 Row를 조회할 때, Lock을 걸어두는 것을 의미한다. 조회를 하는 것이므로, Lock이 걸려있어도, 조회 기능은 수행 가능하지만 수정 및 삭제는 불가능하다.
특정 Row를 수정하고나 삭제할 때, Lock을 걸어두는 것을 의미한다. 수정 및 삭제가 진행되고 있는 상황이기 때문에 수정, 삭제 뿐만 아니라 읽기도 불가능하다.
InnoDB는 계층적 Lock 기능을 제공한다. 예를 들면, 테이블에 Lock이 걸리면 그 하위 개념인 Row들에도 lock이 걸리는 것을 의미한다.
테이블에 Lock을 걸기 위해서 모든 Row에 lock을 거는 비효율을 피하기 위해서 계층적 Locking을 제공한다.
그 중에서, Intention lock은 테이블 수준의 lock이고, Intention shared lock(IS), Intention exclusive lock(IX)으로 구성된다.
Row-level의 Shared lock을 획득하기 전, 해당 테이블에 대한 IS Lock을 먼저 획득하게 되고, 마찬가지로 Row-leveld의 Exclusive lock을 획득하기 전엔 해당 테이블에 대한 IX lock을 먼저 획득하게 된다.
INDEX 레코드에 걸리는 Lock을 의미한다.
SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE;
c1이라는 컬럼에 INDEX가 걸려있고, 위와 같은 쿼리가 수행되는 경우, 쿼리의 결과를 반환할 때까지 해당 INDEX에 대해서 Lock이 걸리게 된다.
쉽게 이야기하면, INDEX의 범위에 Lock을 거는 것을 의미한다.
SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE;
c1이라는 컬럼에 INDEX가 걸려있고, 위와 같은 쿼리가 수행된다고 했을 때, c1의 값이 10이상 20이하에 해당하는 INDEX 테이블 공간 전체에 Lock을 거는 것을 의미한다.
Record lock과 Gap lock의 조합이다. 소스 DB를 DB Replica에 binlog를 기반으로 싱크를 맞출 때 주로 사용된다고 한다.
InnoDB는 기본적으로 REPEATABLE READ
수준의 격리성으로 트랜잭션이 수행된다. 이 때, 검색 및 인덱스 스캔하는 경우 InnoDB는 Next-key lock을 사용한다.
쉽게 이야기하면, 조회 대상이 되는 INDEX 모두에게 Gap lock 또는 Record lock을 거는 것을 의미한다.
Next-key lock은 Phantom rows이슈를 방지해준다.
INSERT 쿼리가 수행될 때 획득하는 Gap lock과 비슷한 형태의 lock이다.
Insert intention lock은 Gap lock과 마찬가지로 INDEX의 범위에 lock을 걸지만, 현재 수행중인 INSERT문에 포함된 INDEX 값이 겹치지 않는다면 Block하지 않고 Insert를 허용한다.
AUTO_INCREMENT
컬럼에 대해서 제한적으로 실행되는 테이블 레벨의 Lock이다. 여러 INSERT 문이 동시에 수행되는 상황에서 INSERT문이 수행되는 테이블에 AUTO_INCREMENT
컬럼이 포함되어 있다면, 해당 컬럼이 순차적으로 Auto increment할 수 있도록 기다리는 Lock을 의미한다.
InnoDB에서는 SPATIAL INDEX
라는 공간 데이터를 위한 인덱싱을 제공하는데, 이 경우에 적용되는 Lock이다.
공간에 대한 범위를 Lock을 걸어야하는 경우에 사용된다.
Lock이 걸려있는 데이터에 어떠한 쿼리를 수행하려고 하면, 기본적으로 Lock이 해제될 때까지 기다려야한다. 기다리는 시간이 너무 길면 서비스의 속도가 느려지고 극단적인 상황에서는 기능이 수행되지 못하고 실패하는 상황도 발생할 수 있다.
Deadlock은 직역하면 '교착 상태'를 의미한다. Deadlock 이슈는 2개 이상의 트랜잭션이 서로에 대한 Lock을 가져가면서 어떠한 트랜잭션도 수행이 안되는 이슈를 의미한다.
innoDB의 경우 Dead lock detector가, Undo log를 기준으로 교착된 트랜잭션 중 하나를 Rollback해주긴 하지만, 이러한 이슈를 만들지 않는 것이 중요하겠다. 🤔
Blocking 이슈와 Deadlock 이슈 모두 무조건 해결할 수 있는 만능 해결법은 존재하지 않고, 이런 이슈들이 발생하지 않도록 옳바르게 쿼리를 짜는 것이 가장 중요하다.
아래의 가이드 라인에 맞게 쿼리를 짜는 연습을 잘 하는 것이 중요하다 👍
SELECT ... FOR UPDATE
, SELECT ... FOR INSERT
와 같은 쿼리는 의도적으로 쓰기 Lock을 걸고 싶은 것이 아니라면, 주의해서 사용UPDATE
, INSERT
문 안에서 SELECT
문을 수행하는 것을 지양