잠금(Lock)은 동시성을 제어하기 위한 기능으로 여러 트랜잭션이 같은 데이터에 접근할 때 데이터 충돌이나 불일치를 방지한다.
트랜잭션은 데이터 정합성을 보장하기 위한 기능이다. 데이터 정합성(Constistency)이란 데이터가 일관성 있게 유지되는 것을 말한다. 이 외에도 원자성(Atomicity), 고립성(Isolation), Durability(지속성) 특징을 가진다.
데이터 무결성은 데이터가 정확한 상태를 유지하는 것을 말한다.
- 사용자 A는 emp_no가 50000이고 first_name이 Lara인 사원을 INSERT 한다.
- 사용자 B가 변경된 내용을 커밋하기도 전에 사용자 B는 emp_no = 50000인 사원을 검색한다.
- 사용자 A가 처리 도중 알 수 없는 문제가 발생해 롤백한다고 해도 여전히 사용자 B는 "Lara"가 정상적인 사원이라고 생각하고 계속 처리할 것이 문제다.
- 사용자 A는 emp_no=50000 사원의 first_name을 "Lara"에서 "Toto"로 변경했다. 이때, 새로운 값인 "Toto"는 테이블에 즉시 기록되고 이전값인 "Lara"는 언두 영역으로 백업된다.
- 사용자 A가 커밋하기 전에 B가 emp_no=50000을 조회하면 first_name 칼럼의 값은 "Toto"가 아니라 "Lara"로 조회된다. (언두 영역에 있는 데이터 조회)
- 사용자 B가 BEGIN 명령으로 트랜잭션을 시작하고 first_name이 "Toto"인 사원을 검색했는데 결과가 없었다. 하지만 사용자 A가 "Toto"로 변경 후 사용자 B가 똑같은 쿼리로 다시 조회하면 결과가 1건 조회된다.
- 사용자 A의 트랜잭션 번호는 12, 사용자 B의 트랜잭션 번호는 10이다.
- 사용자 A는 사원의 이름을 "Toto"로 변경하고 커밋한다.
- 사용자 B는 A 트랜잭션의 변경 전후 각각 한 번씩 SELECT 했지만, 결과는 항상 "Lara"라는 값을 가져온다. B는 자신의 트랜잭션 번호(10)보다 작은 트랜잭션 번호에서 변경한 것만 보게 된다.
A가 employees 테이블에 INSERT를 실행하는 도중에 사용자 B가 SELECT FOR UPDATE 쿼리로 테이블을 조회하는 상황
- B는 BEGIN 명령으로 트랜잭션을 시작한 후 SELECT를 수행한다. 위에서처럼 두 번의 SELECT 쿼리 결과는 똑같아야 한다.
- 하지만 첫 번째 SELECT FOR UPDATE에선 A가 커밋하기 전이므로 "Lara" 결과 한 건만 검색되고, 두 번째 SELECT FOR UPDATE에선 A가 커밋한 이후이므로 새롭게 추가된 "Georgi" 결과까지 추가로 검색된다.
- 쓰기 잠금을 거는 것인데, 언두 레코드에는 쓰기 잠금을 걸 수 없다.
SELECT FOR UPDATE, SHARE
로 조회되는 레코드는 언두 영역의 레코드를 가져오는 것이 아니라 현재 레코드의 값을 가져오기 때문이다.- 위의 예시는 PHANTOM READ의 예외적인 상황이라 설명하고, MySQL REPEATABLE-READ 격리 수준에선 팬텀 리드가 발생하지 않는다고 한다. 그럼 일반적인 팬텀 리드 상황을 생각해 보고, MYSQL에선 어떻게 방지되는지 찾아보자.
FLUSH TABLES WITH READ LOCK
명령으로 획득할 수 있으며, MySQL에서 제공하는 잠금 가운데 가장 범위가 넓다.LOCK TABLES table_name [ READ | WRITE]
해당 명령어를 통해 락을 획득하고, UNLOCK TABLES
로 락을 반납한다.RENAME TABLE tab_a TO tab_b
같이 테이블의 이름을 변경하는 경우 자동으로 획득한다.레코드 자체만을 잠그는 것을 레코드 락이라고 한다. InnoDB에서는 인덱스의 레코드를 잠근다. 인덱스가 없는 테이블이더라도 내부적으로 자동 생성된 클러스터 인덱스를 이용해 잠금을 설정한다.
비관적 쓰기 락(Select For Update), 비관적 읽기 락(Select For Share) 모두 포함하는 개념이다. 아래와 같이 특정 레코드만을 잠그는 쿼리를 생각해볼 수 있다.
SELECT * FROM time_slot WHERE id = 5 FOR UPDATE;
비관적 락을 사용하지 않더라도 REPEATABLE_READ 격리 수준에서 일관된 읽기를 위해 레코드에 읽기 락이 걸린다.
갭 락은 레코드 자체가 아니라 인접한 레코드 사이의 간격만을 잠그는 것을 의미한다.
레코드와 레코드 사이의 간격에 새로운 레코드가 생성되는 것을 제어하며, 갭 락 그 자체보다 넥스트 키 락의 일부로 자주 사용된다.
아래와 같이 1, 5 사이에 있는 레코드(2, 3, 4)를 잠그는 경우를 생각해 볼 수 있다.
SELECT * FROM time_slot WHERE id > 1 AND Id < 5 FOR UPDATE;
비관적 락을 사용하지 않더라도 REPEATABLE_READ 격리 수준에서 일관된 읽기를 위해 특정 조건에 속한 레코드가 락이 걸린다.
레코드 락과 갭 락을 합쳐 놓은 형태의 잠금을 넥스트 키 락이라고 한다.
아래와 같이 1 ~ 5 레코드 (1, 2, 3, 4, 5)를 잠그는 경우를 생각해 볼 수 있다.
SELECT * FROM time_slot WHeRE id >= 1 AND id <= 5 FOR UPDATE;
비관적 락을 사용하지 않더라도 REPEATABLE_READ 격리 수준에서 일관된 읽기를 위해 특정 조건에 속한 레코드가 락이 걸린다.
mysql > SELECT COUNT(*) FROM employees WHERE first_name='Georgi';
+----------+
| COUNT(*) |
+----------+
| 253 |
+----------+
mysql > SELECT COUNT(*) FROM employees WHERE first_name='Georgi' AND last_anme='Klassen';
+----------+
| COUNT(*) |
+----------+
| 1 |
+----------+
mysql > UPDATE employees SET hire_date=NOW() WHERE first_name='Georgi' AND last_name='Klassen';
위와 같이 Update 쿼리를 실행하면 1건의 레코드가 변경될 것이다. 그럼 몇 개의 레코드에 락을 걸어야 할까?
- 조건은 first_name='Georgi'이며, last_name 컬럼은 인덱스에 없기에 first_name='Georgi'인 레코드 253건의 레코드가 모두 잠긴다.
- 만약 테이블에 인덱스가 하나도 없다면? 테이블을 풀 스캔하면서 UPDATE 작업을 하게 된다. 즉 전체 30여만 건의 모든 레코드를 잠그게 된다.
select
r.trx_id waiting_trx_id,
r.trx_mysql_thread_id waiting_thread,
r.trx_query waiting_query,
b.trx_id blocking_trx_id,
b.trx_mysql_thread_id blocking_thread,
b.trx_query blocking_query
FROM
performance_schema.data_lock_waits w
INNER JOIN
information_schema.innodb_trx b
ON
b.trx_id = w.blocking_engin_transaction_id
INNER JOIN
information_schema.innodb_trx r
ON
r.trx_id = w.requesting_engine_transaction_id;
- 현재 대기 중인 스레드는 18, 19번이다.
- 18번 스레드는 17번 스레드를 기다리고 있다.
- 19번 스레드는 17번 스레드와 18번 스레드를 기다리고 있다.
SELECT * FROM performance_schema.data_locks\G