[Real MySQL] 5. 트랜잭션과 락

2024. 2. 27. 13:44CS/Database

트랜잭션

잠금(락)과 트랜잭션은 서로 비슷한 개념 같지만 잠금은 동시성을 제어하기 위한 기능이고, 트랜잭션은 데이터 정합성을 보장하기 위한 기능이다.

  • 락: 여러 커넥션에서 동시에 동일한 자원을 요청할 경우 순서대로 한 시점에는 하나의 커넥션만 변경할 수 있게 해주어 데이터 정합성을 지켜준다.
  • 트랜잭션 격리 수준: 여러 트랜잭션 간의 작업 내용을 어떻게 공유하고 차단할 것인지를 결정하는 레벨을 의미한다.

MySQL에서 MyISAM 스토리지 엔진과 MEMORY 스토리지 엔진은 트랜잭션을 지원하지 않지만 InnoDB 스토리지 엔진은 트랜잭션을 지원한다. 트랜잭션을 지원하지 않는 경우에는 데이터의 정합성을 맞추는 것이 중요하고 어려운 문제가 된다. 하지만 트랜잭션을 지원하는 경우에는 애플리케이션 개발에서 고민해야 할 문제를 줄여준다. 

 

그렇다면, 트랜잭션은 두 개 이상의 쿼리가 있을 때에만 의미가 있을까?

 

➡️ 답은 그렇지 않다. 트랜잭션은 하나의 논리적인 작업 셋이 하나의 쿼리가 있든 두 개 이상의 쿼리가 있든 관계없이 논리적인 작업 셋 자체가 100% 적용되거나(커밋), 아무것도 적용되지 않아야(롤백) 함을 보장해 주는 것이다.

 

MyISAM 테이블과 InnoDB 테이블의 차이를 예로 들어보자. 각 테이블에 pk 값이 3인 레코드를 INSERT 한다고 가정하자.

CREATE TABLE tab_myisam (fdpk INT NOT NULL, PRIMARY KEY(fdpk)) ENGINE=MyISAM;
INSERT INTO tab_myisam (fdpk) VALUES (3);
CREATE TABLE tab_innodb (fdpk INT NOT NULL, PRIMARY KEY(fdpk)) ENGINE=INNODB;
INSERT INTO tab_innodb (fdpk) VALUES (3);

 

그리고 다음과 같이 각 테이블에 데이터를 1, 2, 3을 INSERT 한다.

INSERT INTO tab_myisam (fdpk) VALUES (1), (2), (3);
INSERT INTO tab_innodb (fdpk) VALUES (1), (2), (3);

 

두 INSERT 문장 모두 PK 중복 오류로 쿼리가 실패한다. 그런데 두 테이블의 레코드를 조회해보면, MyISAM 테이블은 오류가 발생했음에도 '1'과 '2'는 INSERT 된 상태로 남아 있다. 즉, MyISAM 테이블에 INSERT 문장이 실행되면서 차례대로 '1'과 '2'를 저장하고, 그다음 '3'을 저장하려고 하는 순간 중복 키 오류가 발생한 것이다. 그리고 이미 INSERT된 '1'과 '2'는 그대로 두고 쿼리 실행이 종료된다.

 

반면, InnoDB는 쿼리 실행 중 일부라도 오류가 발생하면 전체를 롤백한다. 따라서 MyISAM에서 발생하는 부분 업데이트(Partial Update) 현상이 발생하지 않는다.

 

이러한 부분 업데이트 현상의 테이블 데이터의 정합성을 맞추는데 어려운 문제를 만들어 낸다. 부분 업데이트 현상이 발생하면 실패한 쿼리로 인해 남은 레코드를 다시 삭제하는 재처리 작업이 필요할 수 있다. 반면, 트랜잭션을 지원하는 InnoDB를 사용하면 롤백을 통해 손쉽게 데이터 정합성을 맞출 수 있다. 

MySQL이 제공하는 락

MySQL에서 사용되는 락은 크게 스토리지 엔진 레벨과 MySQL 엔진 레벨로 나눌 수 있다. MySQL 엔진 레벨의 잠금은 모든 스토리지 엔진에 영향을 미치지만, 스토리지 엔진 레벨의 잠금은 스토리지 엔진 간 상호 영향을 미치지는 않는다.

MySQL 엔진 락

  • 글로벌 락
    • MySQL에서 제공하는 락 중 가장 범위가 큰 락으로, 한 세션에서 글로벌 락을 획득하면 MySQL 서버에 존재하는 모든 테이블에 락이 걸린다. 즉, 글로벌 락은 MySQL 서버의 모든 변경 작업을 멈춘다.
    • 따라서 웹 서비스용으로 사용되는 MySQL 서버에서는 가급적 사용하지 않는 것이 좋다. 글로벌 락을 획득하기 전에 먼저 테이블이나 레코드에 쓰기 잠금을 거는 SQL이 실행되었다면 해당 SQL과 그 트랜잭션이 완료될 때까지 기다렸다가 글로벌 락이 걸리게 된다. 따라서 최악의 경우, 장시간 실행되는 쿼리와 글로벌 락을 거는 명령어가 실행되면 아주 오랜시간 MySQL 서버의 모든 테이블에 락이 걸린 상태가 지속되어 다음 SQL을 실행하기 위해 오랜 시간 기다려야 할 수 있다. 
  • 테이블 락
    • 개별 테이블 단위로 설정되는 락으로, 명시적 또는 묵시적으로 특정 테이블의 락을 획득할 수 있다.
      • 명시적
        • LOCK TABLES table_name [ READ | WIRTE ] 명령으로 특정 테이블의 락을 획득할 수 있고, UNLOCK TABLES 명령으로 잠금을 해제할 수 있다.
        • 명시적인 테이블 락도 특별한 상황이 아니면 애플리케이션에서 사용할 필요가 거의 없다. 명시적으로 테이블을 잠그는 작업은 글로벌 락과 동일하게 온라인 작업에 상당한 영향을 미치기 때문이다.
      • 묵시적
        • MyISAM이나 MEMORY 테이블의 경우, 테이터를 변경하는 쿼리를 실행하면 해당 데이터의 테이블에 묵시적으로 테이블 락이 걸린다. 
        • InnoDB 테이블의 경우, 레코드 기반의 락을 제공하기 때문에 단순 데이터 변경 쿼리로 인해 테이블 락이 설정되지는 않는다. 정확히는 InnoDB 테이블에도 테이블 락이 묵시적으로 설정될 수 있지만, 대부분의 데이터 변경 쿼리(DML)에서는 무시되고 스키마를 변경하는 쿼리(DDL)의 경우에만 테이블 락이 걸린다.
  • 네임드 락
    • 네임드 락은 GET_LOCK() 함수를 이용해 임의의 문자열에 대해 잠금을 설정할 수 있다.
  •  메타데이터 락
    • 메타데이터 락은 데이터베이스 객체(테이블이나 뷰 등)의 이름이나 구조를 변경하는 경우에 획득하는 잠금이다. 메타데이터 락은 명시적으로 획득할 수 있는 것이 아니고 RENAME TABLE table_name TO table_new_name 같이 테이블의 이름이나 구조를 변경하는 경우 자동으로 획득되는 잠금이다. 

InnoDB 스토리지 엔진 락

InnoDB 스토리지 엔진은 MySQL 엔진에서 제공하는 락과는 별개로 스토리지 엔진 내부에서 레코드 기반의 잠금 방식을 제공한다. 덕분에 MyISAM보다 훨씬 뛰어난 동시성 처리를 제공할 수 있다. 

  • 레코드 락
    • 레코드 자체를 잠그는 것을 레코드 락이라고 하며, 다른 DBMS의 레코드 락과 동일한 역할을 한다.
    • 단, InnoDB 스토리지 엔진은 레코드 자체가 아니라 인덱스를 기반으로 레코드를 찾고 해당 레코드를 잠근다. 따라서 인덱스가 하나도 없는 테이블이더라도 내부적으로 자동 생성된 클러스터 인덱스를 이용해서 잠금을 설정한다. 
  • 갭 락
    • 갭 락은 레코드 자체가 아니라 레코드와 바로 인접한 레코드 사이의 간격만 잠그는 것을 의미한다. 즉, 갭 락은 레코드와 레코드 사이의 간격에 새로운 레코드가 생성되는 것을 제어한다. 갭 락은 그 자체보다는 넥스트 키 락의 일부로 자주 사용된다.
  • 넥스트 키 락
    • 레코드 락과 갭 락을 합쳐 놓은 형태의 잠금이다.

Reference

  • 위키북스, Real MySQL 8.0