InnoDB는 레코드 기반의 잠금 방식을 사용하는데, 중요한 점은 레코드 자체를 잠그는 것이 아니라 인덱스를 잠근다는 것이다. 이 부분은 잘못 설계하면 데이터베이스 성능에 큰 영향을 미칠 것 같아 확실히 알아보기 위해 테스트를 진행해보기로 했다.
먼저 전체적인 시나리오를 설명하면 (first_name, last_name, age) 속성을 가지는 member 테이블을 만들고 last_name 속성에 인덱스를 설정했다. 테이블에 저장된 ‘김민수’ 회원의 나이를 바꾸기 위해 UPDATE하는 트랜잭션을 시작하였다. 이 경우, 동시에 다른 ‘김’씨 성을 가진 회원 정보를 수정할 수 있을까?
얼핏 생각하면 ‘김민수’ 회원 한명의 레코드를 수정하고 있는 것 뿐인데, 김민수 회원이 아닌 다른 ‘김’씨 성을 가진 회원의 레코드를 동시에 수정하는 것은 이상하지 않아 보인다.
그러나 InnoDB의 인덱스 잠금 방식에 따르면 한 트랜잭션에서 ‘김민수’ 회원의 나이를 변경하는 동안 인덱스는 last_name인 ‘김’에 걸려 있으므로 ‘김’씨 성을 가진 모든 회원의 레코드가 잠긴다.
실제로는 어떻게 동작하는지 테스트를 통해 알아보자.
1. (first_name, last_name, age) 컬럼을 가진 member 테이블을 만들면서 last_name 컬럼에 대해 인덱스를 설정해준다.
CREATE TABLE member (
first_name VARCHAR(50),
last_name VARCHAR(50),
age INT;
INDEX idx_last_name (last_name)
);
2. 테이블에 다수의 회원 목록을 더미데이터로 넣는다. 더미데이터 쿼리는 ChatGPT에게 부탁했다.
INSERT INTO member (first_name, last_name, age)
VALUES
('Minsoo', 'Kim', 25),
('Joon', 'Kim', 32),
('Hojin', 'Park', 38),
('Sangmin', 'Kim', 29),
('Taeyang', 'Cho', 37),
('Eunbi', 'Yoo', 31),
...;
3. 생일을 맞은 김민수가 한살 더 먹었으니 나이를 업데이트하는 트랜잭션을 시작한다. 이때 트랜잭션이 자동으로 커밋되지 않도록 autocommit=OFF로 설정해준다.
START TRANSACTION;
UPDATE member SET age=age+1 WHERE last_name='Kim' AND first_name='Minsoo';
4. 앞선 트랜잭션이 commit되거나 rollback되지 않은 상태에서 새로운 트랜잭션에서 김씨 성을 가진 다른 회원의 레코드를 수정해보자.
START TRANSACTION;
UPDATE member SET age=age+1 WHERE last_name='Kim' AND first_name='Joon';
5. 두번째 트랜잭션에서는 잠금 대기 상태가 지속되다가 대기 시간이 초과되었다는 에러가 발생한다.
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
이로써 첫번째 트랜잭션에서 업데이트 대상이 되는 레코드(김민수) 뿐만 아니라 같은 인덱스 값을 가지는 레코드(김준) 또한 잠기는 것을 확인할 수 있었다.
그러면 여러 클라이언트가 UPDATE 요청할 때 동시성이 떨어지지 않으려면 어떻게 인덱스를 설계해야할까?
위 예시를 구현해보면서 느낀 점은 이름에서 성(last_name)과 같이 겹치는 값이 많이 생기는 컬럼에 인덱스를 설정하면 동시성이 상당히 떨어질 것 같다. 따라서 읽기(SELECT)보다 수정 작업(UPDATE)이 많이 발생하는 테이블이라면 겹치는 값이 많이 생기는 컬럼에 인덱스 설정하는 것을 지양해야할 것 같다. 이 경우 인덱스를 설정한다면 auto increment, uuid 같이 고유한 값을 가지는 컬럼을 인덱스로 설정하고, 특정 레코드를 수정할 때는 꼭 필요한 경우가 아니라면 인덱스 설정한 고유한 값을 WHERE 절에 두고 찾아서 수정하는 것이 동시성을 향상시키는 방향이 아닐까 생각한다.
'데이터베이스 > MySQL' 카테고리의 다른 글
[MySQL] 커버링 인덱스 (1) | 2024.02.16 |
---|---|
[MySQL] 실행 계획 확인하는 명령어 (0) | 2024.02.07 |
[MySQL] 옵티마이저의 선택은? 네스티드 루프 조인 vs. 해시 조인 (0) | 2024.02.02 |
[MySQL] 프라이머리 키가 일반 인덱스보다 특별한 이유 - InnoDB 클러스터링 인덱스 (0) | 2024.01.19 |
[MySQL] 시스템 변수 내맘대로 설정해보기 (0) | 2024.01.04 |