Database/MySQL

트랜잭션과 잠금

Debin 2022. 11. 29.
반응형

2023.10.9 복습 리팩토링 시작

트랜잭션

트랜잭션은 작업의 완전성을 보장해 준다. 즉, 정합성을 보장하기 위한 기능이다.

논리적인 작업 셋을 모두 완벽하게 처리하거나,

처리하지 못할 경우에는 원 상태로 복구해서 작업의 일부만 적용되는 현상이 발생하지 않게 만들어준다.

MyISAM과 MEMORY 스토리지 엔진은 트랜잭션을 지원하지 않는다.

 

트랜잭션은 하나의 논리적인 작업 셋에 하나의 쿼리가 있든 두 개 이상의 쿼리가 있든 관계 없이 논리적인 작업 셋 자체가 100% 적용되거나(COMMIT을 실행했을 때) 아무것도 적용되지 않아야 (ROLLBACK 또는 트랜잭션을 ROLLBACK 시키는 오류 발생) 함을 보장해 주는 것이다.

  • InnoDB : 트랜잭션 지원, 부분 업데이트 현상 발생 X
  • MyISAM, MEMORY : 트랜잭션 지원 X, 부분 업데이트 현상 발생한다. 부분 업데이트 현상이란 예를 들어 A라는 트랜잭션 안에 a, b, c라는 쿼리가 있는데 a와 b는 성공적으로 실행하고 c에서 오류가 터지면 a, b라는 쿼리는 DB에 반영하고 쿼리 실행을 종료한다.

트랜잭션 또한 DBMS의 커넥션과 동일하게 꼭 필요한 최소의 코드에만 적용하는 것이 좋다.

이는 프로그램 코드에서 트랜잭션의 범위를 최소화하라는 의미다.

주의할 부분

  • 메일 전송이나 FTP 파일 전송 작업 같은 네트워크를 통해 원격 서버와 통신하는 작업은 트랜잭션 내에서 제거해야만 한다.
  • 단순 조회는 트랜잭션을 사용하지 않아도 무방하다.
  • 꼭 필요한 순간부터 커넥션을 가져와서 트랜잭션을 시작하자.
    책의 예시의 로그인 여부 체크 같은 경우에는 트랜잭션이 필요 없다.
    그러므로 그 이후의 로직인 사용자 정보를 DBMS에 저장하는 부분부터 트랜잭션을 시작하자.

잠금

잠금은 트랜잭션과 비슷한 개념 같지만 사실 동시성을 제어하기 위한 기능이다.

잠금은 여러 커넥션에서 동시에 동일한 자원을 요청할 경우 순서대로 한 시점에는 하나의 커넥션만 변경할 수 있게 해주는 역할을 한다.

MySQL 엔진의 잠금

MySQL에서 사용되는 잠금은 크게 스토리지 엔진 레벨과 MySQL 엔진 레벨로 나눌 수 있다.

MySQL 엔진 레벨의 잠금은 모든 스토리지 엔진에 영향을 미치지만, 스토리지 엔진의 잠금은 스토리지 엔진 간 상호 영향을 미치지 않는다.

글로벌 락

글로벌 락은 아래와 같은 명령어로 획득할 수 있다. MySQL에서 제공하는 잠금 가운데 가장 범위가 크다.

FLUSH TABLES WITH READ LOCK
  • 일단 한 세션에서 글로벌 락을 획득하면 다른 세션에서 SELECT를 제외한 대부분의 DDL 문장이나 DML 문장을 실행하는 경우 글로벌 락이 해제될 때까지 해당 문장이 대기 상태로 남는다.
  • 글로벌 락이 영향을 미치는 범위는 MySQL 서버 전체이며, 작업 대상 테이블이나 데이터베이스가 다르더라도 동일하게 영향을 미친다.
  • 여러 데이터베이스에 존재하는 MyISAM이나 MEMORY 테이블에 대해 mysqldump로 일관된 백업을 받아야 할 때는 글로벌 락을 사용해야 한다.
  • 참고로 글로벌 락은 MySQL 서버의 모든 테이블에 큰 영향을 미치기 때문에 웹 서비스용으로 사용되는 MySQL 서버에서는 가급적 사용하지 않는 것이 좋다.
  • 또한 mysqldump를 이용해 백업을 수행한다면 mysqldump에서 사용하는 옵션에 따라 MySQL 서버에 어떤 잠금을 걸게 되는지 자세히 확인해 보는 것도 좋다.

InnoDB 스토리지 엔진은 트랜잭션을 지원하기 때문에 일관된 데이터 상태를 위해 모든 데이터 변경 작업을 멈출 필요는 없다.

MySQL 8.0부터는 Xtrabackup이나 Enterprise Backup과 같은 백업 툴들의 안정적인 실행을 위해 백업 락이 도입됐다.

LOCK INSTANCE FOR BACKUP;
#백업 실행
UNLOCK INSTANCE;

특정 세션에서 백업 락을 획득하면 모든 세션에서 다음과 같이 테이블의 스키마나 사용자의 인증 관련 정보를 변경할 수 없게 된다.

  • 데이터베이스 및 테이블 등 모든 객체 생성 및 변경, 삭제
  • REPAIR TABLE과 OPTIMIZE TABLE 명령
  • 사용자 관리 및 비밀번호 변경

하지만 백업 락은 일반적인 테이블의 데이터 변경은 허용된다.

일반적인 MySQL 서버의 구성은 소스 서버와 레플리카 서버로 구성되는데, 주로 백업은 레플리카 서버에서 실행된다.

테이블 락

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

명시적으로는 아래 명령으로 특정 테이블의 락을 획득할 수 있다.

LOCK TABLES table_name [ READ | WRITE ]
  • 테이블 락은 MyISAM, InnoDB 스토리지 엔진을 사용하는 테이블에도 동일하게 설정할 수 있다.
  • 명시적으로 획득한 잠금은 UNLOCK TALBLES 명령으로 잠금을 반납(해제)할 수 있다.
  • 명시적 테이블 락도 특별한 상황 아니면 애플리케이션에 사용할 필요가 없다.
  • 글로벌 락과 같이 온라인 작업에 상당한 영향을 미치기 때문이다.
  • 묵시적인 테이블 락은 MyISAM이나 MEMORY 테이블에 데이터를 변경하는 쿼리를 실행하면 발생한다.
  • MySQL 서버가 데이터가 변경되는 테이블에 잠금을 설정하고 데이터를 변경한 후, 즉시 잠금을 해제하는 형태로 사용된다.
  • 즉, 묵시적인 테이블 락은 쿼리가 실행되는 동안 자동으로 획득됐다가 쿼리가 완료된 후 자동 해제된다.
  • 하지만 InnoDB 테이블의 경우 스토리지 엔진 차원에서 레코드 기반의 잠금을 제공하기 때문에 단순 데이터 변경 쿼리로 인해 묵시적인 테이블 락이 설정되지는 않는다.
  • 더 정확히는 InnoDB 테이블에도 테이블 락이 설정되지만 대부분의 데이터 변경 쿼리(DML) 쿼리에서는 무시되고 스키마를 변경하는 쿼리(DDL)의 경우에만 영향을 미친다.

네임드 락

  • 네임드 락은 GET_LOCK() 함수를 이용해 임의의 문자열에 대해 잠금을 설정할 수 있다.
  • 네임드 락은 단순히 사용자가 지정한 문자열에 대해 획득하고 반납하는 잠금이다.
  • 네임드 락은 자주 사용되지 않는다.

예를 들어, 데이터베이스 서버 1대에 5대의 웹 서버가 접속해서 서비스하는 상황에서 5대의 웹 서버가 어떤 정보를 동기화해야 하는 요건처럼 여러 클라이언트가 상호 동기화를 처리해야 할 때 네임드 락을 이용하면 쉽게 해결할 수 있다.

#"mylock"이라는 문자열에 대해 잠금을 획득한다.
#이미 잠금을 사용 중이면 2초 동안만 대기한다. 2초 이후 자동 잠금 해제된다.
SELECT GET_LOCK('mylock', 2);

#"mylock"이라는 문자열에 대해 잠금이 설정돼 있는지 확인한다.
SELECT IS_FREE_LOCK('mylock');

#"mylock"이라는 문자열에 대해 획득했던 잠금을 반납한다.
SELECT RELEASE_LOCK('mylock');

#3개 함수 모두 정상적으로 락을 획득하거나 해제한 경우에는 1을, 아니면 NULL이나 0을 반환한다.

네임드 락의 경우 많은 레코드에 대해서 복잡한 요건으로 레코드를 변경하는 트랜잭션에 유용하게 사용할 수 있다.

메타데이터 락

  • 메타데이터 락은 데이터베이스 객체의 이름이나 구조를 변경하는 경우에 획득하는 잠금이다.
  • 메타데이터 락은 명시적으로 획득하거나 해제할 수 있는 것이 아니고 "RENAME TABLE tab_a TO tab_b" 같이 테이블의 이름을 변경하는 경우 자동으로 획득하는 잠금이다.
  • RENAME TABLE 명령의 경우 원본 이름과 변경될 이름 두 개 모두 한꺼번에 잠금을 설정한다.
  • 메타 데이터 잠금과 InnoDB 트랜잭션을 함께 사용하는 예시가 코드와 함께 책에서 나온다. p.165. 참고하면 좋을 것 같다.
  • 참고로 MySQL 서버의 DDL은 단일 스레드로 작동한다.

InnoDB 스토리지 엔진의 잠금

InnoDB 스토리지 엔진은 레코드 기반의 잠금 기능을 제공하며, 잠금 정보가 상당히 작은 공간으로 관리되기 때문에 레코드 락이 페이지 락으로, 또는 테이블 락으로 레벨업되는 경우(락 에스컬레이션)는 없다.

일반 상용 DBMS와는 조금 다르게 InnoDB 스토리지 엔진에서는 레코드 락 뿐만 아니라 레코드와 레코드 사이의 간격을 잠그는 갭이라는 것이 존재한다.

레코드 락

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

갭 락

  • 다른 DBMS와의 차이가 있는 것이 갭 락이다.
  • 갭 락은 레코드 자체가 아니라 레코드와 바로 인접한 레코드 사이의 간격만을 잠그는 것을 의미한다.
  • 갭 락의 역할은 레코드와 레코드 사이의 간격에 새로운 레코드가 생성되는 것을 제어하는 것이다.
  • 갭 락은 그 자체보다는 밑에 서 언급할 넥스트 키 락의 일부로 사용한다.

넥스트 키 락

  • 레코드 락과 갭 락을 합쳐 놓은 형태의 잠금을 넥스트 키 락이라고 한다.
  • STATEMENT 포맷의 바이너리 로그를 사용하는 MySQL 서버에서는 REPEATABLE READ 격리 수준을 사용해야 한다.
  • 또한 innodb_locks_unsafe_for_binlog 시스템 변수가 비활성화되면 변경을 위해 검색하는 레코드에는 넥스트 키 락 방식으로 잠금이 걸린다.
  • InnoDB의 갭 락이나 넥스트 키 락은 바이너리 로그에 기록되는 쿼리가 레플리카 서버에서 실행될 때 소스 서버에서 만들어 낸 결과와 동일한 결과를 만들어내도록 보장하는 것이 주 목적이다.
  • 그런데 의외로 넥스트 키 락과 갭 락으로 인해 데드락이 발생하거나 다른 트랜잭션을 기다리게 만드는 일이 자주 발생한다.
  • 가능하다면 바이너리 로그 포맷을 ROW 형태로 바꿔서 넥스트 키락이나 갭 락을 줄이는 것이 좋다.

자동 증가 락

  • AUTO_INCREMENT 칼럼이 사용된 테이블에 동시에 여러 레코드가 INSERT 되는 경우, 저장되는 각 레코드는 중복되지 않고 저장된 순서대로 증가하는 일련번호 값을 가져야 한다.
  • InnoDB 스토리지 엔진에서는 이를 위해 내부적으로 AUTO_INCREMENT 락이라고 하는 테이블 수준의 잠금을 사용한다.
  • 자동 증가 락은 INSERT와 REPLACE 쿼리 문장과 같이 새로운 레코드를 저장하는 쿼리에서만 필요하다.
  • InnoDB의 다른 잠금과는 달리 자동 증가 락은 트랜잭션과 관계없이 INSERT나 REPLACE 문장에서 AUTO_INCREMENT 값을 가져오는 순간만 락이 걸렸다가 즉시 해제된다.
  • AUTO_INCREMENT 락은 테이블에 단 하나만 존재하기 때문에 두 개의 INSERT 쿼리가 동시에 실행되는 경우는 하나는 자동 증가 락을 걸면 나머지 쿼리는 자동 증가 락을 기다려야 한다.
  • AUTO_INCREMENT 칼럼에 명시적으로 값을 설정하더라도 자동 증가 락을 걸게 된다고 한다.

인덱스와 잠금

  • Inno DB의 잠금은 레코드를 잠그는 것이 아니라 인덱스를 잠그는 방식으로 처리된다.
  • 즉 변경해야 할 레코드를 찾기 위해 검색한 인덱스의 레코드를 모두 락을 걸어야 한다.
  • 만약 Update 문장을 위한 적절한 인덱스가 준비돼 있지 않다면 각 클라이언트 간의 동시성이 상당히 떨어져서 한 세션에서 Update 작업을 하는 중에는 다른 클라이언트는 그 테이블을 업데이트하지 못하고 기다려야 하는 상황이 발생하는 것이다.

MySQL의 격리 수준

  • 트랜잭션의 격리 수준이라는 것은 하나의 트랜잭션 내에서 또는 여러 트랜잭션 간의 작업 내용을 어떻게 공유하고 차단할 것인지를 결정하는 레벨을 의미한다.
  • 즉 여러 트랜잭션이 동시에 처리 될 때 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 없게 허용할지 말지를 결정하는 것이다.

격리 수준은 크게 아래와 같이 4가지로 나뉜다.

  • READ UNCOMMITTED
  • READ COMMITTED
  • REPEATABLE READ
  • SERIALIZABLE

READ UNCOMMITTED는 거의 사용하지 않는다.

또한 동시성이 중요한 데이터베이스에서는 SERIALIZABLE이 사용되지 않는다.

SERIALIZABLE 격리 수준이 아니라면 크게 MySQL 서버의 성능의 개선이나 저하는 발생하지 않는다.

일반적인 온라인 서비스 용도의 데이터베이스는 READ COMMITTED와 REPEATABLE READ 중 하나를 사용한다.

오라클은 주로 READ COMMITTED 수준을 많이 사용하며, MySQL에서는 REPEATABLE READ를 주로 사용한다.

READ UNCOMMITTED 

  • 어떤 트랜잭션에서 처리한 작업이 완료되지 않았는데도 다른 트랜잭션에서 볼 수 있는 현상을 더티 리드라고 하고,
  • 더티 리드가 허용되는 격리 수준이 READ_UNCOMMITTED다.
  • 굉장히 정합성에 문제가 많은 격리 수준으로 사용하지 않는 것이 좋다.

READ COMMITTED

  • 제일 많이 사용되는 격리 수준이다.
  • 이 격리 수준에서 더티 리드는 발생하지 않는다.
  • 어떤 트랜잭션에서 데이터를 변경했더라도 COMMIT이 완료된 데이터만 다른 트랜잭션에서 조회할 수 있기 때문이다.
  • READ COMMITTED 격리 수준에서도 "NON-REPEATABLE READ" 라는 부정합의 문제가 있다.

NON-REPEATABLE READ 예시

  1. 처음 사용자 B가 BEGIN 명령어로 트랜잭션을 시작하고 성이 '김'인 사용자를 검색했는데 결과가 없었다.
  2. 하지만 사용자 A가 사원 번호가 5인 사원의 이름을 '김'으로 변경하고 커밋을 실행한다.
  3. 사용자 B가 다시 SELECT 쿼리로 성이 '김'인 사용자를 조회했는데 결과가 나왔다.

이는 별다른 문제가 없어 보이지만, 사실 사용자 B가 하나의 트랜잭션 내에서 똑같은 SELECT 쿼리를 실행했을 때는

항상 같은 결과를 가져와야 한다는 "REPEATABLE READ" 정합성에 어긋나는 것이다.

 

이러한 부정합 현상은 일반적인 웹 프로그래밍에서는 크게 문제가 되지 않을 수 도 있지만 하나의 트랜잭션에서 동일 데이터를 여러 번 읽고 변경하는 작업이 금전적인 처리와 연결되면 문제가 될 수 있다.

예를 들어, 다른 트랜잭션에서 입금과 출금 처리가 계속 진행 될 때 다른 트랜잭션에서 오늘 입금된 금액의 총합을 조회한다고 가정해보자.

그런데 "REPEATABLE READ"가 보장되지 않기 때문에 총합을 계산하는 SELECT 쿼리는 실행될 때마다 다른 결과를 가져올 것이다.

중요한 것은 사용중인 트랜잭션의 격리 수준에 의해 실행하는 SQL 문장이 어떤 결과를 가져오게 되는지를 정확히 예측할 수 있어야 한다.

그리고 당연히 이를 위해서는 각 트랜잭션의 격리 수준이 어떻게 작동해야하는지 알아야 한다.

 

READ COMMITTED 격리 수준에서는 트랜잭션 내에서 실행되는 SELECT 문장과 트랜잭션 외부에서 실행되는 SELECT 문장의 차이가별로 없다. 하지만 REPEATABLE READ 격리 수준에서는 기본적으로 SELECT 문장도 트랜잭션 범위 내에서만 작동한다.

즉 START TRANSACTION 명령으로 트랜잭션을 시작한 상태에서 온종일 동일한 쿼리를 반복해서 실행해도 동일한 결과만 보게 된다.

별로 중요하지 않은 것처럼 보이지만 이런 문제로 데이터의 정합성이 깨지고 그로 인해 애플리케이션에 버그가 발생하면 찾아내기가 쉽지 않다.

REPEATABLE READ

  • REPEATABLE READ는 MySQL의 InnoDB 스토리지 엔진에서 기본으로 사용되는 격리 수준이다.
  • 바이너리 로그를 가진 MySQL 서버에서는 최소 REPEATABLE READ 격리 수준 이상을 사용해야 한다.
  • 이 격리 수준에서는 READ COMMITTED 격리 수준에서 발생하는 "NON-REPEATABLE READ" 부정합이 발생하지 않는다.
  • InnoDB 스토리지 엔진은 트랜잭션이 롤백될 가능성에 대비해 변경되기 전 레코드를 언두 공간에 백업해두고 실제 레코드 값을 변경한다. 이는 이전에 학습한 MVCC 방식이다.
  • REPEATABLE READ는 이 MVCC를 위해서 언두 영역에 백업된 이전 데이터를 이용해 동일 트랜잭션 내에서는 동일한 결과를 보여줄 수 있게 보장한다.
  • 사실 READ COMMITTED도 MVCC를 이용해 COMMIT 되기 전의 데이터를 보여준다.
  • REPEATABLE READ와 READ COMMITTED의 차이는 언두 영역에 백업된 레코드의 여러 버전 가운데 몇 번째 이전 버전까지 찾아 들어가야 하느냐에 있다.
  • 모든 InnoDB의 트랜잭션은 고유한 트랜잭션 번호를 가지며, 언두 영역에 백업된 모든 레코드에는 변경을 발생시킨 트랜잭션의 번호가 포함되어 있다.
  • 그리고 언두 영역의 백업된 데이터는 InnoDB 스토리지 엔진이 불필요하다고 판단하는 시점에 주기적으로 삭제한다.
  • REPEATABLE READ 격리 수준에서는 MVCC를 보장하기 위해 실행 중인 트랜잭션 가운데 가장 오래된 트랜잭션 번호보다 트랜잭션 번호가 앞선 언두 영역의 데이터는 삭제할 수가 없다.
  • 그렇다고 가장 오래된 트랜잭션 번호 이전의 트랜잭션에 의해 변경된 모든 언두 데이터가 필요한 것은 아니다.
  • 더 정확하게는 특정 트랜잭션 번호의 구간 내에서 백업된 언두 데이터가 보존돼야 한다.

동작 방식을 살펴보자.

  1. employee 테이블의 초기 두 레코드는 번호가 6인 트랜잭션에 의해 INSERT 됐다.
  2. 사용자 B가 번호가 10인 트랜잭션을 사용해 emp_no가  50인 데이터 레코드를 SELECT 했다.
    컬럼 이름의 값은 Lara.
  3. 사용자 A는 번호가 12인 트랜잭션을 사용해 emp_no가 50인 레코드의 이름 컬럼을 Toto로 바꿨다. (Lara -> Toto)
    이를 커밋함.
  4. 번호가 10인 트랜잭션이 다시 emp_no가 50인 데이터 레코드를 가져옴. 이름 컬럼은 Toto가 아니라 Lara였다. 놀랍다.

사용자 B가 트랜잭션을 시작하면서 10이라는 트랜잭션 번호를 부여 받았는데,

그때부터 사용자 B의 10번 트랜잭션 안에서 실행되는 모든 SELECT 쿼리는 트랜잭션 번호가 10보다 작은 트랜잭션 번호 안에서 변경한 것만 보게 된다.

트랜잭션 번호가 12인 테이블 레코드(Toto)를 가져온 것이 아니라, 트랜잭션 번호가 6인 언두로그 레코드(Lara)를 가져와버린 것이다.

 

사실 하나의 레코드에 대한 백업은 하나 이상 얼마든지 존재할 수 있다.

한 사용자가 BEGIN으로 트랜잭션을 시작하고 장시간 트랜잭션을 종료하지 않으면 언두 영역이 백업된 데이터로 무한정 커질 수도 있다.

이렇게 언두 백업된 레코드가 많아지면 MySQL 서버의 처리 성능이 떨어질 수 있다.

 

REPEATABLE READ 격리 수준에서도 다음과 같은 부정합이 발생할 수 있다.

  1. employee 테이블의 초기 두 레코드는 번호가 6인 트랜잭션에 의해 INSERT 됐다.
  2. 사용자 B가 번호가 10인 트랜잭션을 사용해 emp_no가  50 이상인 데이터 레코드를 SELECT FOR UPDATE 쿼리를 진행했다.
    조회한 데이터 레코드의 이름 컬럼 값은 Lara.
  3. 사용자 A가 번호가 12인 트랜잭션을 사용해 emp_no가 51인 데이터를 INSERT 했다. 이름 컬럼 값은 Georgi
  4. 번호가 10인 트랜잭션을 사용해 emp_no가 50 이상인 데이터 레코드를 SELECT FOR UPDATE하면 결과는 
    Lara, Georgi 결과 2건이 나오게 된다.

Georgi가 이름인 테이블 레코드는 최신 트랜잭션 번호가 12라 어디서 꺼내올 레코드가 없다.

결국 동일한 트랜잭션에서 진행한 같은 SELECT FOR UPDATE 쿼리는 같아야 하는데 첫 번째 쿼리는 값이 Lara이고, 두 번째 쿼리는 값이 Lara, Georgi가 된다.

 

이렇게 다른 트랜잭션에서 수행한 변경 작업에 의해 레코드가 보였다 안 보였다 하는 현상을  PHANTOM READ, PHANTOM ROW라고 한다.

 

SELECT FOR UPDATE는 레코드에 쓰기 잠금을 걸어여 하는데, 언두 레코드에는 잠금을 걸 수가 없다.

그래서 SELECT FOR UPDATE나 SELECT LOCK IN SHARE MODE로 조회되는 레코드는

언두 영역의 변경 전 데이터를 가져오는 것이 아니라 현재 레코드의 값을 가져오게 되는 것이다.

SERIALIZABLE

  • 가장 단순한 격리 수준이면서 동시에 가장 엄격한 격리 수준이다.
  • 그만큼 동시 처리 성능도 다른 트랜잭션 격리 수준보다 떨어진다.
  • InnoDB 테이블에서 기본적으로 순수한 SELECT은 아무런 레코드 잠금도 설정하지 않고 실행된다.
  • InnoDB 메뉴얼에서 나오는 "Non-locking consistent read"라는 말이 이를 의미한다.
  • 하지만 해당 격리 수준으로 설정되면 읽기 작업도 공유 잠금(읽기 잠금)을 획득해야 하며, 동시에 다른 트랜잭션은 그러한 레코드를 변경하지 못하게 된다.
  • 즉, 한 트랜잭션에서 읽고 쓰는 레코드를 다른 트랜잭션에서는 절대 접근할 수 없는 것이다.
  • SERIALIZABLE 격리 수준에서는 팬텀 리드가 발생하지 않는다.

하지만 InnoDB 스토리지 엔진에서는 갭 락과 넥스트 키 락 덕분에 REPEATABLE READ 격리 수준에서도 팬텀 리드가 발생하지 않는다.

 

참고 자료

REAL MySQL 8.0 1권

 

 

2023.10.10 복습 리팩토링 마무리

반응형

댓글