RealMySQL 책을 읽고 노션에 정리한 글을 옮긴 것입니다.
MyISAM 스토리지 vs InnoDB 스토리지의 트랜잭션 차이
mysql> CREATE TABLE tab_myisam (fdpk INT NOT NULL, PRIMARY KEY (fdpk) ENGINE=MyISAM;
mysql> INSERT INTO tab_myisam (fdpk) VALUES (3);
mysql> CREATE TABLE tab_innodb ( fdpk INT NOT NULL, PRIMARY KEY (fdpk) ENGINE=INNODB;
mysql> INSERT INTO tab_innodb (fdpk) VALUES (3);
--// AUTO-COMMIT 활성화
mysql> SET autocommit=ON;
mysql> INSERT INTO tab_myisam (fdpk) VALUES (1), (2), (3);
ERROR 1062 (23000) : Duplicate entry '3' for key 'PRIMARY'
mysql> INSERT INTO tab_innodb (fdpk) VALUES (1), (2), (3);
ERROR 1062 (23000) : Duplicate entry '3' for key 'PRIMARY'
mysql> SELECT * FROM tab_myisam;
+------+
| fpdk |
+------+
| 1 |
| 2 |
| 3 |
+------+
mysql> SELECT * FROM tab_innodb;
+------+
| fpdk |
+------+
| 3 |
+------+
MyISAM 스토리지 엔진과 InnoDB 스토리지 엔진 둘다 프라이머리 키 중복 오류로 INSERT 가 실패했다.
MyISAM 스토리지 엔진은 쿼리가 실패 했음에도 불구하고 ‘1’ 과 ‘2’ 가 INSERT 됐는데, 그 이유는 1, 2 차례대로 저장하고 ‘3’을 저장하는 순간 중복 키 오류가 발생한 것이다. 하지만 1, 2 는 이미 저장됐기 때문에 1, 2는 그대로 나두고 종료를 한다.
InnoDB 는 트랜잭션을 지원하기 때문에 1, 2 를 진행하고 3 을 진행하는 와중에 오류가 발생시 실행 전 상태로 복구한다.
MyISAM 의 트랜잭션 없이 저장되는 것을 부분 업데이트라고 한다. 부분 업데이트 된것을 롤백 처리하려면 코드가 길어지고 더러워진다.
DMS 트랜잭션 처리에 좋지 않은 영향을 미치는 부분
SQL 자체를 락한다. 다른 데이터베이스도 영향을 받는다.
MyISAM, MEMORY 스토리지 과 연관된 테이블을 mysqldump로 백업 받으려고 할때 글로벌 락을 사용한다.
글로벌 락을 걸기 전에 진행 되고 있는 쓰기 작업을 다 완료된 후에 글로벌 락이 작동한다. 글로벌 락을 실행전에 테이블을 플러시 한다. 즉 관련된 데이터들을 디스크로 플러시한다.
SELECT 쿼리도 종료될 때까지 기다린다 - 왜?
FLUSH TABLES WITH READ LOCK
이 SELECT 쿼리 도중에 읽기 잠금을 강제로 설정하면FLUSH TABLES WITH READ LOCK
은 실행 중인 모든 쿼리가 종료된 후에야 읽기 잠금을 설정한다.글로벌 락은 MySQL 서버의 모든 작업을 멈추기 때문에 좀 더 나은 상태를 위해서 8.0 버전 부터는 조금 더 가벼운 글로벌 락의 필요성이 생겼다. 백업 락이 도입 됐다.
백업락을 획득하면, 데이터베이스 및 테이블 등 모든 객체 생성 및 변경, 삭제가 안되며 REPAIR TABLE과 OPTIMIZE TABLE 명령, 사용자 관리 및 비밀번호 변경이 되지 않는다.
개별 테이블 단위로 설정되는 잠금이며 , 묵시적 경우, 명시적인 경우가 있다.
명시적인 테이블락은 LOCK TABLES table_name [ READ | WRITE ]
명령으로 특정 테이블을 락 할 수 있다.
묵시적인 테이블 락은 MyISAM이나 MEMORY 스토리지 테이블이 데이터 변경할때 사용된다. 자동적으로 테이블에 락을 걸어서 데이터를 변경후 자동으로 해제한다.
- InnoDB 스토리지 엔진은 레코드 기반 락이 있기 때문에 데이터 변경 쿼리 만으로 테이블 락을 걸지 않는다. DML 이 아닌 DDL (스키마 변경) 인 경우에 테이블 락을 건다.
GET_LOCK()
함수를 통해 임의의 문자열에 대한 잠금을 설정할 수 있다.
-- 세션 1에서 'batch_update' 락을 설정
SELECT GET_LOCK('batch_update', 10);
-- 락을 보유한 상태에서 대량 작업 수행
UPDATE my_table SET column1 = 'new_value' WHERE column2 > 100;
-- 작업 완료 후 락 해제
SELECT RELEASE_LOCK('batch_update');
-- 세션 2에서 'batch_update' 락을 요청
SELECT GET_LOCK('batch_update', 10);
-- 세션 1이 락을 해제하기 전까지 대기하거나, 10초가 초과되면 실패
-- 락을 획득한 후 작업 수행
UPDATE my_table SET column1 = 'another_value' WHERE column2 <= 100;
-- 작업 완료 후 락 해제
SELECT RELEASE_LOCK('batch_update');
결국 batch_update
라는 이름으로 락을 걸었고 그 사이에 있는
UPDATE my_table SET column1 = 'new_value' WHERE column2 > 100;
의 작업에 관한거는 다른 세션에서 건드릴 수 없다.
데이터베이스 객체(대표적으로 테이블이나 뷰 등)의 이름이나 구조를 변경하는 경우에 획득하는 잠금
레코드 기반 잠금 → 페이지 잠금 → 테이블 잠금 으로 업데이트 되는 식으로 작동 되지 않는다.
레코드락 + 갭락 을 합쳐 놓은 형태
REPEATABLE READ
와 같은 높은 일관성을 유지하면서도 팬텀 리드(Phantom Read)
를 방지하기 위해 석례된 독특한 잠금 매커니즘이다.
특정 레코드(인덱스 엔트리)
와 그 앞 뒤 간격(갭)
을 잠그는 방식이다.팬텀 리드란?
팬텀 리드는, 트랜잭션이 동일한 SELECT 쿼리를 두 번 실행했을 때, 처음 실행 시에는 없던 새로운 레코드가 두 번째 실행에서 나타나는 현상을 말합니다.
-- 트랜잭션 1
START TRANSACTION;
SELECT * FROM orders WHERE id > 3;
-- 트랜잭션 2 (동시에 실행)
START TRANSACTION;
INSERT INTO orders (id, product_name, quantity) VALUES (4, 'Elderberry', 20);
COMMIT;
-- 트랜잭션 1이 다시 실행
SELECT * FROM orders WHERE id > 3;
SELECT
에서는 id > 3
에 해당하는 레코드가 없었지만, 트랜잭션 2에서 새로운 레코드 id = 4
가 삽입된 후, 두 번째 SELECT
에서 새롭게 나타납니다.넥스트 키 락은 레코드와 갭(Gap)을 잠금으로 보호하여, 다른 트랜잭션이 해당 범위에 레코드를 삽입하거나 수정하지 못하도록 막습니다. 이를 통해 팬텀 리드가 발생하지 않습니다.
InnoDB의 잠금은 레코드를 잠그는것이 아니라 인덱스를 잠그는 것이다.
PK 1개이므로 PK 인덱스 엔트리가 잠길 수 있고 Secondary Index 는 여러개일 수 있기 때문에 복수로 잠길 수 도 있다.
// first_name 칼럼만 맴버로 담긴 ix_firstname이라는 인덱스가 준비돼 있다.
// KEY ix_firstname (first_name), last_name 인덱스는 없음
// employees 테이블에서 first_name='Georgi' 인 사원은 전체 253명이 있으며,
// first_name='Georgi' 이고 last_name='Klassen' 인 사원은 1명만 있는 것을
// 아래 쿼리로 확인할 수 있다.
mysql> SELECT count(*) FROM employees WHERE first_name='Georgi';
253
mysql > SELECT COUNT(*) FROM employees WHERE first_name='Georgi' AND last_name='Klassen';
1
// employees 테이블에서 first_name='Georgi' 이고 last_name='Klassen'인 사원의 입사일자를
// 오늘로 변경하는 쿼리를 실행해보자.
mysql> UPDATE employees SET hire_date=NOW() WHERE first_name='Georgi'
AND last_name = 'Klassen';
// 이렇게 실행하면 UPDATE 를 하기 위해서 first_name='Georgi' 인 애들253 개가 레코드 락이 걸린다
// last_name 이란 인덱스가 없기 때문에 253 개가 잠금이 걸리는것이다.
DIRTY READ | NON-REPEATABLE READ | PHANTOM READ | |
---|---|---|---|
READ UNCOMMITTED | 발생 | 발생 | 발생 |
READ COMMITTED | 없음 | 발생 | 발생 |
REPEATABLE READ | 없음 | 없음 | 발생(InnoDB 는 없음) |
SERIALIZABLE | 없음 | 없음 | 없음 |
COMMIT
이 나 ROLLBACK
여부에 상관없이 다른 트랜잭션에서 보인다.dirty read
라고 한다.dirty read
같은 상황이 발생하지 않는다.사용자 A 가 emp_no 가 50000 인 first_name 이 “Lara” 의 이름을 Toto 로 변경을 했다.
그 즉시 버퍼 풀에 있는 더티페이지에 emp_no 가 50000 인 first_name 을 “Toto”로 생성한다.
기존에 “Lara” 는 롤백이나 다른 트랜잭션의 참고를 위해 언두 로그에 emp_no 가 50000 인 값의 first_name이 “Lara”인 것을 생성한다.
사용자 B 는 트랜잭션을 생성해서 emp_no 가 50000 인 값을 읽으면 언두 로그에 있는 값을 읽게 된다.
사용자 A 가 COMMIT 을 하는 경우에 다른 트랜잭션이 emp_no = 500000 의 값을 호출하면 그 후에는 “Toto” 값을 읽게 된다.
READ COMMITTED 에서”NON-REPEATABLE READ”라는 부정합의 문제가 있다.
예_ 다른 트랜잭션에서 입금과 출금 처리가 계속 진행될 때 다른 트랜잭션에서 오늘 입금된 금액의 총합을 조회 하는 경우
- 이 경우
REPEATABLE READ
가 보장되지 않기 때문에 총합을 계산하는 SELECT 쿼리는 실행될 때마다 다른 결과를 가져오게 된다.
트랜잭션 번호 6번으로 emp_no 가 50000인 값의 first_name 을 Lara로 Insert 했다.
사용자 B 가 트랜잭션 10번 번호로 생성해서 emp_no=50000 인 값을 SELECT 했다.
사용자 A가 사용자 B 트랜잭션이 아직 COMMIT 되지 않은 상태에서 트랜잭션 12번 번호로 생성해서 emp_no 가 500000 d인 값의 first_name 을 Toto 로 변경하고 COMMIT 을 했다.
언두로그에 이전 값을 복사하였으며 생성시켰던 트랜잭션 번호 6번도 그대로 언두로그에 생성된다.
사용자 B 가 다시 emp_no 가 50000인 값을 조회 했을때 10번 트랜잭션은 자기 트랜잭션 번호보다 높은 값의 데이터를 조회하는게 아니라 낮은 값 즉 언두로그에 있는 값을 조회한다.
REPEATABLE READ 격리 수준에서도 다음과 같은 부정합이 발생할 수 있다.
넥스트 키 락
이 존재한다.SELECT … FOR UPDATE
로 인덱스 범위를 잠그면 조건에 해당하는 레코드(Record Lock)와 그 범위(Gap Lock)이 모두 잠긴다.