[정리] 트랜잭션과 MySQL 엔진 잠금

img_1.png

DB 잠금에 관해 공부한 내용을 정리해본다. 이 글의 내용은 MySQL을 기준으로 설명한다.

트랜잭션

트랜잭션은 작업의 완전성을 보장하는 개념이다. 논리적인 작업 셋을 모두 완료하거나 원상 복구시킨다. 부분적 업데이트를 허용하지 않는다.

트랜잭션은 하나의 논리적인 작업 셋에 쿼리가 한개든 두개든 관계없이 논리적인 작업셋 전체가 100% 적용되거나(COMMIT 실행 시) 아무것도 적용되지 않음(ROLLBACK 실행 시)을 보장한다.

부분적 업데이트를 허용하지 않음.

InnoDB 엔진의 경우 트랜잭션 내에서 하나라도 오류가 발생하면 ROLLBACK 처리된다.

트랜잭션 주의사항

트랜잭션은 DBMS의 커넥션과 동일하게 귀중한 자원이므로, 꼭 필요한 최소의 코드에만 적용하자.

즉, 코드가 DB 커넥션을 가지는 범위, 트랜잭션이 활성화 되는 범위를 최소화하자는 것임.

또한, 코드가 짧더라도 네트워크 I/O가 있는 경우 반드시 트랜잭션 범위에서 배제하자. 만약 네트워크 I/O에서 병목이 발생하면 트랜잭션, 커넥션 낭비가 발생한다.

잠금과 동시성 제어

잠금은 동시성 제어를 위해 존재하고, 트랜잭션은 데이터 정합성을 보장하기 위해 존재한다. "동시성 제어"와 "데이터 정합성"은 전혀 다른 개념임을 유념하고 잠금과 동시성 제어를 혼동하지 말자.

잠금은 여러 커넥션이 동시에 하나의 자원을 요청할 경우 순서대로 한 시점에는 하나의 커넥션만 접근하게 해주는 것이다. 운영체제에서 공부하던 잠금개념이고 Critical Section에 다중 스레드 접근을 제어하는 기술이다.

격리 수준

나중에 나올 내용이고 이전에 공부해봤는 격리 수준도 간단히 정리해보자. 격리 수준은 하나의 트랜재션내에서 또는 여러 트랜잭션 간의 작업 내용을 어떻게 공유하고 차단할 것인지 결정하는 수준(Level)을 의미한다.

이 격리수준은 다음 글에서 다시한번 더 정리해보려 한다.

MySQL 엔진 잠금

MySQL이 사용하는 잠금은 크게 "MySQL 엔진", "스토리지 엔진"으로 나뉜다.

MySQL엔진 수준의 잠금은 모든 스토리지 엔진에 영향을 끼친다. 반면, 스토리지 엔진 수준의 잠금은 MySQL에 영향을 못 끼친다.

MySQL엔진의 락은 다음 종류가 있다.

  • 글로벌 락: 세션 단위에 락 걸기 위함
  • 테이블 락: 테이블 데이터 동기화 위함
  • 메타데이터 락: 테이블 구조 잠금 위함
  • 네임드 락: 사용자 필요에 맞게 사용 위함

글로벌 락

FLUSH TABLES WITH READ LOCK으로 글로벌 락 획득이 가능하다.

MySQL이 제공하는 락 중에서 가장 범위가 큰 락.

한 세션에서 글로벌 락 획득시 다른 세션에서 SELECT를 제외한 대부분의 DDL, DML 실행 시, 글로벌 락이 해제될 때 까지 대기한다.

글로벌 락이 영향을 미치는 범위는 MySQL 서버 전체다. 작업 대상 테이블, 데이터베이스가 달라도 락의 영향을 받는다.

여러 DB에 존재하는 MyISMA, MEMORY 스토리지 엔진의 테이블에 대해 mysqldump로 일관된 백업 받아야 하는경우에서 이 글로벌 락 사용한다.

테이블 락

테이블 락은 개별 테이블 단위로 락을 건다.

명시적 또는 묵시적으로 특정 테이블의 락을 획득 가능하다.

명시적으로 락을 얻기 위한 명령어는 LOCK TABLES table_name [ READ | WRITE ]이다. 이러면 특정 테이블의 락 획득이 가능하다. 락 해제는 UNLOCK TABLES로 가능하다.

이 테이블 락은 사용에 주의해야되는게 글로벌 락과 마찬가지로 온라인 작업에 상당히 큰 영향을 끼치기 때문이다.

네임드 락

네임드 락은 GET_LOCK() 함수로 임의 문자열에 대한 잠금 설정이 가능하다.

이 잠금의 특징은 대상이 테이블, 레코드, AUTO_INCREMENT ? 같은 DB객체가 아닌것이다.

단순히 사용자가 지정한 문자열에 대해 잠금을 획득하고 반환하는 형태.

RealMySQL 내용에 의하면 이 락은 자주 사용되지 않는다한다.

네임드 락 사용 예제를 한번 보겠다. 다음과 같은 상황을 가정한다.

DB 서버 1대, 웹 서버 5대가 있는 상황일 때, 5대의 웹 서버가 어떤 정보를 DB로 부터 동기화해야 될 때 네임드 락을 사용하면 쉽게 해결가능하다.

분산 락을 네임드 락으로 구현했다 생각하면 이해가 편하다.

만약 5대의 웹 서버가 동시에 "하루치 통계 데이터 생성"이라는 무거운 쿼리를 실행한다고 가정해 보자. 5대가 동시에 DB를 때리면 DB CPU가 과부하가 걸릴 가능성이 크다.

이때, 네임드 락을 활용해볼 수 있는데 다음과 같다.

  1. 모든 웹 서버는 작업을 시작하기 전에 GET_LOCK('daily_stat_job', 10)을 호출
  2. 가장 먼저 도착한 1대만 락을 획득하고 작업을 시작
  3. 나머지 4대는 락을 못 얻었으므로, "아, 다른 서버가 하고 있구나" 하고 대기하거나 작업을 건너뜀

별도의 복잡한 소프트웨어(Redis 등) 설치 없이, MySQL 함수 하나만으로 여러 서버 간의 동기화 제어가 가능해서 유용하다.

또한, 많은 레코드에서 복잡한 요건으로 레코드를 변경하는 트랜잭션에서도 유용히 사용이 가능하다.

배치 작업처럼 한번에 많은 레코드 변경하는 쿼리에서는 이 네임드 락이 데드락의 원인이 되곤한다.

데드락의 원이이 되는 이유를 좀 더 자세히 설명해보겠다.

UPDATE 경우 레코드 단위의 락을 잡는다.

  • 트랜잭션 A: 1번 레코드 잠금 -> 2번 레코드 잠금 시도
  • 트랜잭션 B: 2번 레코드 잠금 -> 1번 레코드 잠금 시도
  • 결과: 데드락 발생

이때, 애플리케이션 레벨에서 "이 범위의 데이터를 건드릴 때는 'mylock_part_1'이라는 이름을 가진 녀석만 들어와!" 라고 네임드 락을 걸어버리면, DB 내부의 레코드 락 순서와 상관없이 한 번에 하나의 세션만 진입하게 되므로 데드락을 원천 차단할 수 있게 된다.

이런 경우, 동일 데이터를 변경, 참조하는 작업끼리 분류해서 네임드 락을 걸고 쿼리를 실행하면 해결이된다.

메타데이터 락

메타데이터 락은 DB 객체(테이블, 뷰 등)의 이름이나 구조를 변경하는 경우에 획득하는 잠금이다.

메타데이터 락은 명시적으로 획득 및 반환이 되는 종류의 락이 아니다.

RENAME TABLE a TO b 같은 쿼리가 실행시 자동으로 획득되는 락.

RENAME TABLE a TO b 의 경우 원본 이름과 변경될 이름 두 개 모두가 락이 걸림

때때로 메타데이터 잠금과 InnoDB 트랜잭션을 동시에 써야되는 경우가있는데 꽤 복잡하지만 천천히 살펴보자.

  1. CREATE TABLE access_log (...) 가 존재
  2. 테이블 생성이니 저장만 하면되서, UPDATE, DELETE 없음
  3. 그런데 어느날 테이블 구조 변경 요건이 생김
  4. Online DDL로 해결이 되긴하지만 오래 걸리는 경우 UNDO 로그 증가와 누적되는 Online DDL 버퍼 크기등 고려할 게 많음
  5. 더 큰 문제는 MySQL DDL은 단일 스레드 동작이기에 시간이 오래걸릴것이 예상됨
  6. 이때는 새로운 구조의 테이블을 생성하고 최근의 데이터(1시간 전 혹은 하루)의 데이터까지를 PK 기준으로 범위별로 나눠 여려개의 스레드로 빠르게 복사시킨다.
  7. 이 복사하는 경우에 INSERT INTO ... SELECT ... 구문이 쓰는데, 이 작업 자체가 하나의 트랜잭션으로 묶여야 데이터 정합성이 깨지는걸 방지할 수 있음

참고자료