
요즘 락에 대해 배우다보니 용어가 너무 많다. 한번 정리하고 가자.
→ 동시성 제어 전략의 큰 분류이다.
충돌이 자주 날 것인지에 대한 생각으로 전략을 세울 수 있다.
JPA(하이버네이트)가 @Version, @Lock으로 구현할 수 있게 도와준다.
DBMS에서 이론적으로 락은 크게 공유 락과 배타 락으로 분류할 수 있다.
SELECT * FROM table_name WHERE id = 1 FOR SHARE;
→ 읽기 전용 락이다.
다른 트랜잭션에서도 공유 락을 획득할 수 있지만, 배타 락은 획득할 수 없다.
즉, 공유 락을 사용하면 트랜잭션 내에서 조회한 데이터가 변경되지 않는다는 것을 보장합니다.
SELECT * FROM table_name WHERE id = 1 FOR UPDATE;
→ 쓰기 전용 락이다.
배타 락이 걸린 데이터에 대해서 다른 트랜잭션에서는 공유 락과 배타 락을 획득할 수 없다.
즉, 배타 락은 독점권을 가지는 락이다.
정리하자면,
낙관적/비관적 = 싸움 날지 말지 생각 방식
공유락/배타락 = 싸움 날 때 규칙(읽기만 가능, 쓰기까지 가능)
이제 MySQL에서 제공하는 락에 대해서 알아보자.
DBMS마다 스토리지 엔진이 다르니까, 어떤 락을 지원하는지도 달라진다.
엔진레벨 락은 다른 스토리지에도 영향을 미친다.
MySQL에서 가장 큰 범위로, MySQL 서버 전체에 적용된다.
단순조회 빼고는 대부분의 명령어 실행할 수 없다.
트랜잭션을 지원하지 않았을 적에 데이터 정합성을 위해 더 자주 사용되었었다.
다음과 같은 상황에서 사용할 수 있다.
InnoDB
Storage Engine: 데이터베이스에서 데이터를 접근하는 방식
InnoDB가 MySQL 기본 스토리지 엔진으로 채택되면서
트랜잭션과 MVCC를 활용해 글로벌 락 없이도 백업 시 정합성을 보장할 수 있게 되었다.
트랜잭션을 지원하는 InnoDB가 상용화되면서, 글로벌 락보다 자주 사용되었다.
단순 조회 + 데이터 수정 명령까지 허용한다.
수정된 데이터는 로그에 기록 후 트랜잭션 종료 시 일괄 반영하는 방법이다.
테이블을 생성/삭제하거나 사용자 권한등은 여전히 바꿀 수 없다.
백업 락은 일반적인 데이터 변경은 허용하면서도, 물리적 구조 변경으로 인한 문제를 예방할 수 있어서 글로벌 락보다 범위가 더 작은 백업 락이 실용적인 선택이 될 수 있다.
다음과 같은 상황에서 사용할 수 있다.

명시적/묵시적으로 테이블을 잠글 수 있다.
(1) 명시적으로 테이블을 잠그는 것은 글로벌 락처럼 하나의 테이블이라는 큰 범위에 영향을 주기 때문에 특별한 상황이 아니라면 사용하지 않는 것을 권장한다.
(2) 묵시적 테이블 락은 데이터가 변경되는 테이블에 잠금을 자동으로 획득한다.
단, InooDB에서는 데이터 변경으로는 테이블락을 획득할 수 없다.
→ InnoDB에서는 레코드 기반 잠금을 지원하기 때문이다.
스토리지레벨 락은 다른 스토리지에 영향을 미치지않는다.
MySQL 5.7버전부터는 InnoDB가 기본 설정이다.
InnoDB 스토리지 엔진은 레코드 기반의 잠금 기능을 제공한다.
잠금의 범위가 MySQL 엔진 잠금에 비해 훨씬 작다.

InnoDB는 레코드 자체가 아니라 인덱스의 레코드를 잠근다.
인덱스가 하나도 없는 테이블이라면 내부적으로 자동 생성된 클러스터 인덱스를 이용해 잠금을 설정한다.
인덱스가 없는 컬럼을 조건절(WHERE)로 사용해 잠금을 설정하면
인덱스가 없어서 테이블 전체 스캔 필요하고 조건에 해당하는 모든 레코드에 대해 잠금이 걸려 테이블 락과 비슷환 효과가 발생하고 데드락이 발생할 확률이 높아지므로 주의할 필요가 있다.
-- PK 조건에 맞는 레코드를 UPDATE 시 (UPDATE시 MySQL이 배타 락을 검)
UPDATE users SET name = 'kaki' WHERE id = 1;
=> id = 1인 레코드에만 정확히 락이 걸림
-- 세컨더리 인덱스 조건에 맞는 레코드를 UPDATE 시 (email에 인덱스가 있다고 가정)
UPDATE users SET name = 'kaki' WHERE email = 'kaki@email.com';
=> email = 'kaki@email.com'인 레코드에 정확히 락이 걸림
-- 복합 인덱스 조건에 맞는 레코드를 UPDATE 시 (email, age 복합 인덱스가 있다고 가정)
UPDATE users SET name = 'kaki' WHERE email = 'kaki@email.com' AND age = 28;
=> email = 'kaki@email.com' AND age = 28인 레코드에 락이 걸림
UPDATE users SET status = 'active' WHERE age = 20;
=> 다음과 같은 과정이 일어남
1. 전체 테이블 스캔을 수행하여 age = 20인 모든 레코드를 찾는다.
2. 조건에 맞는 각 레코드에 락을 건다 (정확히는 해당 레코드의 PK 인덱스에 락을 건다)
3. 일치하는 모든 레코드에 업데이트를 수행
불필요하게 많은 레코드를 읽고 잠그게 되어 성능 저하와 동시성 저하가 일어난다..
원래는 MySQL이 기본적으로 실수로 인한 대량 데이터 수정을 방지하기 위한 Safe Update Mode가 활성화 되어 있어 아래와 같은 에러가 발생할 것이다.
Error Code: 1175. You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column.
To disable safe mode, toggle the option in Preferences -> SQL Editor and reconnect.
갭 락은 레코드와 인접한 레코드 사이에 레코드 생성을 하지 못하게 하는 것이다. 레코드 자체에 락은 걸리지 않는다.
갭 락은 단독으로 사용되기 보다는 넥스트 키 락의 일부로 함께 사용된다.
SELECT *
FROM ACCOUNT
WHERE '통잔잔고' BETWEEN 10,000 AND 9,999,999,999
FOR UPDATE;

위와 같은 쿼리를 작성하면 해당 레코드들 사이에 새로운 값이 들어오지 못한다.
레코드 락과 갭 락을 합친 것이라고 볼 수 있다.
MySQL의 트랜잭션 격리수준(Isolation Level)의 기본값은
REPEATABLE READ이다. 이전에 언급한것처럼 MySQL에서는 Phantom Read가 왠만하면 발생하지 않는다고 하였는데, 레코드 락과 갭 락때문이다.
MySQL 기본 격리 수준인 REPEATABLE_READ을 사용하면 넥스트 키 락을 사용한다.
> ✨ 넥스트 키 락이 발생하는 경우
PK나 Unique 제약조건이 있어 결과가 1개임이 보장될 경우에는 레코드 락이 걸린다.
반면 결과 개수와 상관없이 Unique 제약조건이 없는 경우 넥스트 키 락이 걸린다.Unique 인덱스인데 값이 없을 경우에도 넥스트 키 락이 걸린다.
또한 쿼리가 1개의 결과를 보장하지 않는 “복합 인덱스 중 일부 컬럼만 WHERE절 사용, BETWEEN 같은 연산자 사용” 같은 경우에도 넥스트 키 락이 걸린다.

위와 같이 ranking에 인덱스를 설정한 테이블이 있다고 가정하자.
SELECT * FROM restaurant
WHERE ranking >= 4
FOR UPDATE;
INSERT INTO (raking, restaurant_id, name)
VALUES (3, 9999, '샤브올데이');
랭킹4등 이상인 식당들을 조회하고, 랭킹3등인 샤브올데이를 추가하는 쿼리이다.

위와 같이 4등 이상인 레코드에 대한 레코드 락이 걸리고,
2등과 4등사이, 4등과 5등사이, 5등과 6등사이에 갭 락이 걸리게 되는걸 넥스트 키 락이라고 한다.
데이터 동시성을 효과적으로 관리하고, 행 사이의 간격도 잠궈 팬텀 리드와 같은 이상 현상을 방지하고, 동시성 트랜잭션의 데이터 일관성을 보장하는 락 방법이다.
보통 MySQL을 사용하면 InnoDB를 사용하게 된다.
위에서 설명했듯이 스토리지 엔진 레벨 락은 인덱스 기반의 레코드 잠금을 지원하는데 예시를 통해 고려해야하는 이유를 알아보자.
SELECT count(*) FROM USERS WHERE first_name = '철수';
+------+
| 2000 |
+------+
철수라는 이름을 가진 유저가 2,000명이 있다고 가정하자.
SELECT count(*) FROM USERS
WHERE first_name = '철수' AND last_name = '안';
+------+
| 1 |
+------+
안철수라는 이름을 가진 유저는 1명이다.
UPDATE USERS
SET last_name ='창호'
WHERE first_name = '철수' AND last_name ='안';
안철수라는 유저를 찾아서 last_name을 창호로 변경하고자 한다.

이 때, 인덱스가 없으면 테이블락과 같이 모든 레코드에 락이 걸리게 된다.
동시성 제어는 되지만 응답속도가 상당히 느려질 수 있다는 단점이 생기게 된다.

만약, first_name에 인덱스 설정이 되어있다면
철수이름을 가진 2,000개의 데이터에 대한 조건절에 대한 레코드가 락에 걸리게 된다.
테이블 락보다는 응답속도가 향상되었지만 2,000개에 대한 락이 있기 때문에 동시성제어를 효율적으로 했다고 볼 수는 없다고 생각한다.

first_name, last_name에 복합인덱스로 설정되어있다면
철수/안을 가진 1개 데이터에 대한 조건절에 대한 레코드 락이 걸리게 된다.
1개에 대한 락만 있기 때문에 응답속도가 상대적으로 빠를 것이다.

먼저 초록색의 TransactionA가 초록색 영역을 넥스트 키 락으로 권한을 획득했고,
파란색의 TransactionB 영역을 수정하고자 하는 쿼리를 날렸을 때이다.
TransactionB도 마찬가지로 TransactionA의 영역을 수정하고자 할 때, 데드락이 발생한다.

InnoDB는 미리 데드락 감지를 하여 뒤늦게 실행된 TransactionB를 자동으로 롤백시킨다.
따라서, TransactionB는 실패하게 된다!
MySQL의 Lock 종류와 동작 방식을 파헤쳐 보자
넥스트 키 락(Next-Key Lock) in Database Systems
매일메일 - 공유 락과 배타 락에 대해서 설명해주세요.
[10분 테코톡] 두둠, 쥬니의 데이터베이스 락
[10분 테코톡] ⛲️ 오즈의 데이터베이스 Lock