SELECT FOR UPDATE NOWAIT
동작
잠금 충돌 시 반응
조회하는 레코드가 이미 다른 트랜잭션에 의해 잠겨있는 경우
쿼리가 대기하지 않고 즉시 에러를 반환한다.
에러 메시지
Lock wait timeout exceeded 같은 메시지 출력
InnoDB innodb_lock_wait_timeout 설정와 유사하다
해당 설정은 InnoDB 스토리지의 엔진에서 사용하는 설정이다
트랜잭션이 락
을 기다리는 최대 시간을 정의하는데해당 시간을 초과하면 락을 획득하지 못한 트랜잭션이 실패한다.
기본값은 50초이다.
최솟값은 1초이다.
NOWAIT 쿼리 옵션은
해당 값을 0으로 설정한 효과와 동일하다.
innodb_lock_wait_timeout 옵션은 명시적으로 0으로 설정이 불가능하기에
NOWAIT 옵션이 대체한다.
트랜잭션 유지
쿼리가 에러로 실패해도 트랜잭션이 닫히지가 않는다
명시적으로
COMMIT
혹은ROLLBACK
을 수행해야한다.
장점
잠금을 기다리지 않고 즉시 결과를 확인가능하다.
대체 로직을 실행할 수 있다.
불필요한 대기 시간을 줄일 수 있음.
언제쓰노
잠금 대기 자체가 비정상적인 상황인 경우 사용하면 유용하다.
실습
start transaction;
select *
from departments
where dept_no = 'd001' for
update nowait;
-- dept_no = 'd001' is locked
-- 다른 세션에서
start transaction;
select *
from departments
where dept_no = 'd001' for
update nowait;
-- 에러반환
[2025-03-22 22:55:04] [HY000][3572] Statement aborted because lock(s) could not be acquired immediately and NOWAIT is set.
SELECT FOR UPDATE SKIP LOCKED
동작 방식
잠금 충돌 시 반응
조회하려는 레코드 중에 이미 잠긴 레코드는 건너뛴다.
잠기지 않은 레코드만 잠가서 반환.
결과 크기
잠긴 레코드가 제외
예상보다 적은 결과가 반환될 수 있음.
모든 레코드가 잠기면 빈 결과 집합이 반환된다고 함.
주의
데이터가 반환되지 않더라도 갭락이 발생할 수 있따.
갭락??
InnoDB 스토리지 엔진에서 사용되는 잠금 매커니즘의 하나라고 한다.
인덱스 레코드 사이의 간격(gap) 을 잠그는 행위라고 한다
인덱스 레코드 사이의 간격은
실제 데이터 행이 존재하는 않는 범위
를 말하는 데예를 들어
select ~ where id > 7 인 경우
실제로 데이터는 id 15 20 이 존재하는 데 없는 행인
8 9 등의 값에 대해도 잠금을 하는 역할임.
없는 값을 읽는 팬텀 리드 등을 방지.
ex
트랜잭션이 특정 조건을 만족하는 레코드를 조회하고 수정하는 동안에
다른 트랜잭션이 그 범위에 새로운 데이터를 추가하지 못하도록 보호하는 역할
언제 쓰노
선착순 처리할떄
쿠폰 발급이나 작업큐 등에서 여러 트랜잭션이 동시에 사용가능한 리소스를 가져가야하는 경우 사용함.
ORDER BY 와 LIMIT 사용시
특정 순서로 데이터를 처리하면서 잠긴 레코드를 건너뛰고자 하는 경우 자주 사용된다.
실습
start transaction;
select *
from departments
limit 1
for
update skip locked;
+-------+---------+---------+
|dept_no|dept_name|emp_count|
+-------+---------+---------+
|d001 |Marketing|null |
+-------+---------+---------+
--
다른 세션에서 조회하기
start transaction;
select *
from departments
limit 1
for
update skip locked;
+-------+---------+---------+
|dept_no|dept_name|emp_count|
+-------+---------+---------+
|d002 |Finance |null |
+-------+---------+---------+
d001 은 이미 세션1에 의해 락이 걸려있다.
d001 을 조회할 수 없으니 d002 를 조회하게 된다.
select *
from departments
limit 1
for
update nowait ; 해보면
당연
[HY000][3572] Statement aborted because lock(s) could not be acquired immediately and NOWAIT is set.
에러가 발생한다.
조인과 같이 사용하기
기본적인 동작
잠금 범위
SELECT FOR UPDATE
조인된 모든 테이블의 레코드에 대해 잠금을 수행한다.
문제
1:N 관계에서 부모 테이블(1) 이 잠기면 자식 테이블(N) 이 모든 관련 레코드가 영향을 받는다.
문제 상황
start transaction;
select *
from departments d
join dept_emp de on d.dept_no = de.dept_no
where d.dept_no = 'd001'
limit 1
for
update skip locked;
//
+-------+---------+---------+------+-------+----------+----------+
|dept_no|dept_name|emp_count|emp_no|dept_no|from_date |to_date |
+-------+---------+---------+------+-------+----------+----------+
|d001 |Marketing|null |10017 |d001 |1993-08-03|9999-01-01|
+-------+---------+---------+------+-------+----------+----------+
// 다른 세션에서 작업
start transaction;
select *
from departments d
join dept_emp de on d.dept_no = de.dept_no
where d.dept_no = 'd001'
limit 1
for
update skip locked;
// 빈 레코드 반환
부모테이블이 잠겨있기에 하위 자식 테이블에 대한 내용도 조회를 할 수가 없음;;
동시성이 너무 떨어진다.
해결 방안
OF 구문을 사용하면 된다.
역할
잠글 테이블을 명시적으로 지정해 불필요한 잠금을 방지할 수 있다.
수정 쿼리
start transaction; select * from departments d join dept_emp de on d.dept_no = de.dept_no where d.dept_no = 'd001' limit 1 for update of de skip locked; +-------+---------+---------+------+-------+----------+----------+ |dept_no|dept_name|emp_count|emp_no|dept_no|from_date |to_date | +-------+---------+---------+------+-------+----------+----------+ |d001 |Marketing|null |10017 |d001 |1993-08-03|9999-01-01| +-------+---------+---------+------+-------+----------+----------+ // 다른 세션 start transaction; select * from departments d join dept_emp de on d.dept_no = de.dept_no where d.dept_no = 'd001' limit 1 for update of de skip locked; +-------+---------+---------+------+-------+----------+----------+ |dept_no|dept_name|emp_count|emp_no|dept_no|from_date |to_date | +-------+---------+---------+------+-------+----------+----------+ |d001 |Marketing|null |10055 |d001 |1992-04-27|1995-07-22| +-------+---------+---------+------+-------+----------+----------+
de 테이블의 첫번 째 레코드는 잠겨있기에 skip 처리하고
다음 행을 조회하여 해당 결과를 반환한다.