[Real MySQL] 05. 트랜잭션과 잠금

예니·2023년 2월 4일
0

Real MySQL

목록 보기
4/9
post-thumbnail
  • 트랜잭션은 작업의 완전성을 보장해주는 것이다. 논리적인 작업 셋을 모두 완벽하게 처리하거나, 처리하지 못할 경우에는 원 상태로 복구해서 작업의 일부만 적용되는 현상이 발생하지 않게 만들어주는 기능이다.
  • 잠금은 동시성을 제어하기 위한 기능이고 트랜잭션은 데이터의 정합성을 보장하기 위한 기능이다.
  • 잠금은 여러 커넥션에서 동시에 동일한 자원을 요청할 경우 순서대로 한 시점에는 하나의 커넥션만 변경할 수 있게 해주는 역할을 한다.
  • 격리 수준이라는 것은 하나의 트랜잭션 내에서 또는 여러 트랜잭션 간의 작업 내용을 어떻게 공유하고 차단할 것인지를 결정하는 레벨을 의미한다.

5.1 트랜잭션

5.1.1 MySQL에서의 트랜잭션

InnoDB는 쿼리 중 일부라도 오류가 발생하면 전체를 원 상태로 만든다는 트랜잭션의 원칙대로 INSERT 문장을 실행하기 전 상태로 그대로 복구한다. MyISAM 테이블에서는 부분 업데이트를 수행하며, 이는 테이블 데이터의 정합성을 맞추는 데 상당히 어려운 문제를 만들어낸다. (부분 업데이트는 1,2,3을 한 트랜잭션에서 업데이트할 때, 3 수정 시에 오류가 발생한다면 1,2 수정은 유지되는 것)

5.1.2 주의사항

트랜잭션도 DBMS의 커넥션과 동일하게 꼭 필요한 최소의 코드에만 적용하는 것이 좋다. 즉, 트랜잭션의 범위를 최소화해야 한다.

  • 트랜잭션 처리에 좋지 않은 영향을 미치는 부분
    • 데이터베이스 커넥션은 개수가 제한적이어서 각 단위 프로그램이 커넥션을 소유하는 시간이 길어질수록 사용 가능한 여유 커넥션의 개수는 줄어든다.
    • 네트워크를 통해 원격 서버와 통신하는 등의 작업은 트랜잭션 내에서 제하는 것이 좋다.
    • 단순 조회는 별도로 트랜잭션을 사용하지 않아도 된다.

5.2 MySQL 엔진의 잠금

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

5.2.1 글로벌 락

  • 글로벌 락은 FLUSH TABLES WITH READ LOCK 명령으로 획득할 수 있다.
  • 잠금 가운데 가장 범위가 크다. 범위는 MySQL 서버 전체이며, 작업 대상 테이블이나 데이터베이스가 다르더라도 동일하게 영향을 미친다.
  • 일관된 백업을 받아야 할 때 글로벌 락을 사용한다.
  • 조금 더 가벼운 글로벌 락인 백업 락이 도입됐다. 특정 세션에서 백업 락을 획득하면 모든 세션에서 테이블 스키마나 사용자 인증 관련 정보를 변경할 수 없게 된다. 하지만 백업 락은 일반적은 테이블의 데이터 변경은 허용된다.

5.2.2 테이블 락

테이블 락은 개별 테이블 단위로 설정되는 잠금이며, 명시적 또는 묵시적으로 특정 테이블의 락을 획득할 수 있다.

  • 명시적 LOCK TABLES table_name [ READ | WRITE ]명령으로 획득할 수 있다. 테이블 락은 MyISAM 뿐 아니라 InnoDB 스토리지 엔진을 사용하는 테이블도 동일하게 설정할 수 있다. 명시적으로 획득한 잠금은 UNLOCK TABLES 명령으로 잠금을 반납할 수 있다.
  • 묵시적 MyISAM이나 MEMORY 테이블에 데이터를 변경하는 쿼리를 실행하면 발생한다. 테이블에 잠금을 설정하고 데이터를 변경한 후, 즉시 잠금을 해제하는 형태다. 묵시적인 테이블 락은 쿼리가 실행되는 동안 자동으로 획득됐다가 쿼리가 완료된 후 자동 해제된다. InnoDB 테이블의 경우 스토리지 엔진 차원에서 레코드 기반의 잠금을 제공하기 때문에 묵시적 테이블 락이 설정되지 않는다.

5.2.3 네임드 락

  • 네임드 락은 GET_LOCK() 함수를 이용해 임의의 문자열에 대해 잠금을 설정할 수 있다. 대상이 테이블이나 레코드나 데이터베이스 객체가 아니고, 자용자가 지정한 문자열에 대해 획득하고 반납하는 잠금이다.
  • 네임드 락은 중첩해서 사용할 수 있고, 현재 세션에서 획득한 네임드 락을 한 번에 모두 해제할 수 있다.

5.2.3 메타데이터 락

메타데이터 락은 데이터베이스 객체의 이름이나 구조를 변경하는 경우에 획득하는 잠금이다.

5.3 InnoDB 스토리지 엔진 잠금

  • InnoDB는 레코드 기반의 잠금 방식 때문에 MyISAM보다 훨씬 뛰어난 동시성 처리를 제공할 수 있다.
  • MySQL 서버의 information_schema 데이터베이스에 존제하는 INNODB_TRX, INNODB_LOCKS, INNODB_LOCK_WAITS 테이블을 조인해서 조회하면 현재 어떤 트랜잭션이 어떤 잠금을 대기하고 있고, 해당 잠금을 어느 트랜잭션이 가지고 있는지 조회할 수 있고, 장시간 잠금을 가지고 있는 클라이언트를 찾아서 종료시킬 수 있다.

5.3.1 InnoDB 스토리지 엔진의 잠금

레코드 기반 잠금 기능을 제공하며, 잠금 정보가 상당히 작은 공간으로 관리되기 때문에 레코드 락이 페이지 락으로, 페이지 락이 테이블 락으로 레벨업되는 일은 없다.

5.3.1.1 레코드 락

  • 레코드 자체만을 잠그는 것
  • InnoDB 스토리지 엔진은 레코드 자체가 아니라 인덱스의 레코드를 잠근다. 인덱스가 없는 테이블이더라도 내부적으로 자동 생성된 클러스터 인덱스를 이용해 잠금을 설정한다.
  • 보조 인덱스를 이용한 변경 작업은 넥스트 키 락, 갭 락을 사용하고, 프라이머리 키 또는 유니크 인덱스에 의한 변경 작업에서는 레코드 자체에 대해서만 락을 건다.

5.3.1.2 갭 락

레코드 자체가 아니라 레코드와 바로 인접한 레코드 사이의 간격만을 잠그는 것을 의미한다. 갭 락의 역할은 레코드와 레코드 사이 간격에 새로운 레코드가 생성되는 것을 제어하는 것이다.

5.3.1.3 넥스트 키 락

  • 레코트 락과 갭 락을 합쳐놓은 형태의 잠금
  • InnoDB의 갭 락이나 넥스트 키 락은 바이너리 로그에 기록되는 쿼리가 레플리카 서버에서 실행될 때 소스 서버에서 만들어낸 결과와 동일한 결과를 만들어내도록 보장하는 것이 주 목적이다.

5.3.1.4 자동 증가 락

  • AUTO_INCREMENT 칼럼이 사용된 테이블에 동시에 여러 레코드가 삽입되는 경우, 저장되는 각 레코드는 중복되지 않고 저장된 순서대로 증가하는 일련번호 값을 가져야 한다. 이를 위해 사용하는 것이 자동 증가 락이라는 테이블 수준의 잠금이다.
  • 자동 증가 락은 트랜잭션과 관계없이 INSERT나 REPLACE 문장에서 AUTO_INCREMENT 값을 가져오는 순간만 락이 걸렸다가 즉시 해제된다.
  • 자동 증가 값이 한 번 증가하면 절대 줄어들지 않는 이유가 AUTO_INCREMENT 잠금을 최소화하기 위해서다.

5.3.2 인덱스와 잠금

  • InnoDB의 잠금은 레코드를 잠그는 것이 아니라 인덱스를 잠그는 방식이다. 변경해야 할 레코드를 찾기 위해 검색한 인덱스의 레코드를 모두 락을 건다. update할 대상이 1건이지만, 같은 인덱스로 200건이 있다면 200건 모두에 락이 걸린다. 즉, 적절히 인덱스가 준비돼 있지 않다면 각 클라이언트 간의 동시성이 상당히 떨어질 것이다.
  • 인덱스가 하나도 없다면 풀 스캔하게 된다.
  • MySQL의 InnoDB에서 인덱스 설계가 매우 중요하다.

5.3.3 레코드 수준의 잠금 확인 및 해제

  • InnoDB 스토리지 엔진을 사용하는 테이블의 레코드 수준 잠금은 테이블 수준의 잠금보다 복잡하다.
  • MySQL 5.1부터는 레코드 잠금과 잠금 대기에 대한 조회가 가능하여, 쿼리 하나만 실행해보면 잠금과 잠금 대기를 확인할 수 있다.
  • 강제로 잠금을 해제하려면 KILL 명령으로 MySQL 서버의 프로세스를 강제로 종료하면 된다.
  • 각 트랜잭션이 어떤 잠금을 기다리고 있는지, 기다리고 있는 잠금을 어떤 트랜잭션이 가지고 있는지를 쉽게 메타 정보를 통해 조회할 수 있다. performance_schema 데이터베이스의 data_locks, data_lock_waits 테이블

5.4 MySQL의 격리 수준

  • 트랜잭션의 격리 수준이란 여러 트랜잭션이 동시에 처리될 때 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 있게 허용할지 말지를 결정하는 것이다.
  • DIRTY READ라고도 하는 READ UNCOMMITTED는 일반적인 데이터베이스에서 거의 사용하지 않고, SERIALIZABLE 또한 동시성이 중요한 데이터베이스에서는 거의 사용되지 않는다.
  • 성능은 SERIALIZABLE 격리 수준이 아니라면 크게 성능의 개선이나 저하는 발생하지 않는다.
DIRTY READNON-REPEATABLE READPHANTOM READ
READ UNCOMMITTED발생발생발생
READ COMMITTED없음발생발생
REPEATBLE READ없음없음발생
SERIALIZABLE없음없음없음

InnoDB에서는 독특한 특성 때문에 REPEATABLE READ 격리 수준에서도 PHANTOM READ가 발생하지 않는다.

5.4.1 READ UNCOMMITTED

어떤 트랜잭션에서 처리한 작업이 완료되지 않았는데도 다른 트랜잭션에서 볼 수 있는 현상을 더티 리드라고 한다. READ UNCOMMITTED에서는 더티 리드가 허용된다.

5.4.2 READ COMMITTED

  • 어떤 트랜잭션에서 데이터를 변경했더라도 COMMIT이 완료된 데이터만 다른 트랜잭션에서 조회할 수 있다.
  • 트랜잭션 내에서 실행되는 SELECT와 트랜잭션 없이 실행되는 SELECT의 차이 READ COMMITTED 격리 수준에서는 트랜잭션 내에서 실행되는 SELECT 문장과 트랜잭션 외부에서 실행되는 SELECT 문장의 차이가 별로 없다. 하지만 REPEATABLE READ 수준에서는 기본적으로 SELECT 쿼리 문장도 트랜잭션 범위 내에서만 작동한다. 즉, BEGIN (START TRANSACTION) 명령으로 트랜잭션을 시작한 상태에서 온종일 동일한 쿼리를 반복해서 실행해봐도 동일한 결과만 보게 된다.

5.4.3 REPEATABLE READ

  • InnoDB 스토리지 엔진은 트랜잭션이 롤백될 가능성에 대비해 변경 전 레코드를 언두 공간에 백업해두고 실제 레코드 값을 변경한다. 이를 MVCC라고 한다. REPEATABLE READ는 MVCC를 위해 언두 영역에 백업된 이전 데이터를 이용해 동일 트랜잭션 내에서는 동일한 결과를 보여줄 수 있게 보장한다.
  • BEGIN으로 트랜잭션을 시작하면서 트랜잭션 번호를 부여받고, 그 트랜잭션 안에서 실행되는 모든 SELECT 쿼리는 트랜잭션 번호가 자신보다 작은 트랜잭션 번호에서 변경한 것만 보게 된다.
  • 다른 트랜잭션에서 수행한 변경 작업에 의해 레코드가 보였다 안 보였다 하는 현상을 PHANTOM READ라고 한다. SELECT … FOR UPDATE 쿼리는 SELECT하는 레코드에 쓰기 잠금을 걸어야 하는데, 언두 레코드에는 잠금을 걸 수 없다. 그래서 SELECT … FOR UPDATE 쿼리는 SELECT … LOCK IN SHARE MODE로 조회되는 레코드는 언두 영역의 변경 전 데이터를 가져오는 것이 아니라 현재 레코드의 값을 가져오게 되는 것이다.

5.4.4 SERIALIZABLE

한 트랜잭션에서 읽고 쓰는 레코드를 다른 트랜잭션에서는 절대 접근할 수 없다.

하지만 InnoDB 스토리지 엔진에서는 갭 락과 넥스트 키 락 덕분에 REPEATABLE READ 격리 수준에서도 이미 PHANTOM READ가 발생하지 않기 때문에 굳이 SERIALIZABLE를 사용할 필요는 없다.

0개의 댓글