Lock wait timeout 대응: innodb_lock_wait_timeout과 애플리케이션 재시도 정책
MySQL 행 잠금 대기 제한의 동작 원리와 진단 방법, 안전한 트랜잭션 재시도 정책을 운영 관점에서 정리한다.
1. 왜 Lock wait timeout을 재시도 문제로 보아야 하는가
동시에 같은 행을 변경하는 트랜잭션이 있으면 InnoDB는 정합성을 지키기 위해 한쪽 트랜잭션을 기다리게 한다. 대기가 짧게 끝나면 정상적인 직렬화 비용이지만, 잠금을 보유한 트랜잭션이 느리거나 멈춰 있으면 대기 세션도 연결과 작업 스레드, 메모리, 애플리케이션 요청 슬롯을 계속 점유한다. innodb_lock_wait_timeout은 이러한 InnoDB 행 잠금 대기의 상한을 정하는 변수다.
시간이 다 지나면 대기하던 문장은 다음 오류를 받는다.
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
이 오류에서 중요한 단어는 transaction이다. 실패한 UPDATE 한 문장만 다시 실행하면 된다는 뜻이 아니다. 기본 설정에서는 timeout이 발생한 문장만 롤백되고 그 전에 같은 트랜잭션에서 성공한 변경은 남아 있을 수 있다. 따라서 애플리케이션이 오류를 잡아 같은 연결에서 다음 문장을 계속 실행하거나 그대로 COMMIT하면, 업무 단위의 일부만 반영되는 부분 성공이 발생할 수 있다.
운영 대응은 다음 세 문제를 함께 해결해야 한다.
- 어떤 트랜잭션이 누구를 기다리는지 빠르게 식별한다.
- 잠금 보유 시간을 줄여 timeout 자체를 감소시킨다.
- timeout이 발생해도 업무 트랜잭션 전체를 안전하게 재시도한다.
2. InnoDB 잠금 대기와 timeout의 실행 경로
행을 변경하거나 locking read를 수행할 때 필요한 record lock, gap lock, next-key lock이 이미 충돌 상태라면 요청 트랜잭션은 lock wait 큐에 들어간다. 잠금이 해제되기 전에 대기 시간이 세션의 innodb_lock_wait_timeout에 도달하면 서버는 오류 1205를 반환한다.
graph TD
A[애플리케이션이 트랜잭션 시작] --> B[행 변경 또는 locking read]
B --> C{충돌 잠금 존재?}
C -- 아니요 --> D[잠금 획득 후 문장 실행]
C -- 예 --> E[InnoDB lock wait 큐 진입]
E --> F{보유 트랜잭션이 먼저 종료?}
F -- 예 --> D
F -- 아니요 --> G{대기 시간이 한도에 도달?}
G -- 아니요 --> E
G -- 예 --> H[오류 1205 반환]
H --> I[현재 문장 롤백]
I --> J[애플리케이션이 트랜잭션 전체 ROLLBACK]
J --> K[backoff와 jitter 후 새 트랜잭션으로 재시도]
2.1 세션 값이 실제 대기 한도를 결정한다
innodb_lock_wait_timeout은 세션 단위로 바꿀 수 있다. SET GLOBAL로 변경한 값은 이후 생성되는 세션의 기본값에 영향을 주지만, 이미 연결된 세션이 가진 값을 일괄 변경하지는 않는다. 연결 풀을 사용하는 애플리케이션에서는 DB 전역값만 바꾸고 끝내지 말고 실제 업무 연결의 세션값을 확인해야 한다.
다음 쿼리는 현재 검증 환경의 세션값과 전역 기본값을 함께 확인한다.
SELECT VERSION() AS mysql_version,
@@SESSION.innodb_lock_wait_timeout AS session_timeout_seconds,
@@GLOBAL.innodb_lock_wait_timeout AS global_timeout_seconds,
@@GLOBAL.innodb_rollback_on_timeout AS rollback_on_timeout;
실행 결과(MySQL 8.0.x):
mysql> SELECT VERSION() AS mysql_version,
-> @@SESSION.innodb_lock_wait_timeout AS session_timeout_seconds,
-> @@GLOBAL.innodb_lock_wait_timeout AS global_timeout_seconds,
-> @@GLOBAL.innodb_rollback_on_timeout AS rollback_on_timeout;
+---------------+-------------------------+------------------------+---------------------+
| mysql_version | session_timeout_seconds | global_timeout_seconds | rollback_on_timeout |
+---------------+-------------------------+------------------------+---------------------+
| 8.0.46 | 50 | 50 | 0 |
+---------------+-------------------------+------------------------+---------------------+
1 row in set (0.00 sec)
innodb_rollback_on_timeout이 기본값인 OFF라면 timeout 문장만 롤백된다. 이 변수에 기대어 애플리케이션의 트랜잭션 경계를 느슨하게 관리해서는 안 된다. 서버 버전, 배포 방식, 재시작 옵션에 따라 설정 관리 방법이 달라질 수 있으므로, 애플리케이션은 오류 1205를 받으면 명시적으로 전체 트랜잭션을 ROLLBACK하는 편이 안전하다.
2.2 Deadlock과 Lock wait timeout은 다른 사건이다
Deadlock은 둘 이상의 트랜잭션이 서로가 가진 잠금을 기다려 진행할 수 없는 순환 관계다. innodb_deadlock_detect=ON이면 InnoDB가 순환을 탐지해 희생 트랜잭션 하나를 즉시 오류 1213으로 중단한다. 일반적인 lock wait timeout은 순환이 없어도 발생하며, 대기 시간이 설정값을 넘었을 때 오류 1205가 반환된다.
| 구분 | Lock wait timeout | Deadlock |
|---|---|---|
| 대표 오류 | 1205 | 1213 |
| 발생 조건 | 잠금 대기가 설정 시간을 초과 | 대기 관계가 순환 |
| 일반적인 종료 시점 | timeout 만료 시점 | 탐지 직후 |
| 기본 롤백 범위 | 실패 문장 | 희생 트랜잭션 전체 |
| 애플리케이션 원칙 | 전체 업무 트랜잭션 롤백 후 재시도 | 전체 업무 트랜잭션 재시도 |
두 오류 모두 일시적 충돌일 수 있지만, 재시도를 무제한 허용해서는 안 된다. 특히 timeout은 긴 트랜잭션, 누락된 인덱스, 사용자 입력을 기다리는 트랜잭션처럼 구조적 원인의 신호일 수 있다.
2.3 Metadata Lock에는 다른 timeout이 적용된다
innodb_lock_wait_timeout은 InnoDB 행 잠금 대기를 대상으로 한다. ALTER TABLE이 오래 기다리는 상황처럼 Metadata Lock 대기에는 lock_wait_timeout이 관련된다. “DB 잠금 대기”라는 이유만으로 innodb_lock_wait_timeout을 조정하면 문제를 해결할 수 있다고 판단해서는 안 된다. 먼저 performance_schema.data_lock_waits, performance_schema.metadata_locks, performance_schema.threads 가운데 어느 관측점에서 대기가 보이는지 구분해야 한다.
3. 문장만 롤백되는 동작을 재현하기
다음 예제는 전용 MySQL 8.0 검증 인스턴스에서 Event Scheduler를 별도 서버 세션으로 사용한다. 이벤트 세션이 id=1 행을 변경한 채 잠금을 보유하고, 주 세션은 먼저 id=2를 변경한 다음 id=1을 기다린다. 두 번째 UPDATE가 오류 1205를 만나도 첫 번째 UPDATE는 트랜잭션 안에 남으며, 그대로 COMMIT하면 일부 변경이 반영되는 것을 확인할 수 있다.
Event Scheduler를 이용한 구성은 단일 검증 컨테이너에서 동시 세션을 재현하기 위한 것이다. 운영 서버에서 예제 실행을 위해
event_scheduler를 켜지 않는다.
SET GLOBAL event_scheduler = ON;
DROP EVENT IF EXISTS ev_hold_lock;
DROP PROCEDURE IF EXISTS run_timeout_demo;
DROP TABLE IF EXISTS lock_timeout_demo;
CREATE TABLE lock_timeout_demo (
id BIGINT PRIMARY KEY,
balance INT NOT NULL
) ENGINE=InnoDB;
INSERT INTO lock_timeout_demo (id, balance)
VALUES (1, 100), (2, 200);
DELIMITER //
CREATE EVENT ev_hold_lock
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 2 SECOND
DO
BEGIN
START TRANSACTION;
UPDATE lock_timeout_demo
SET balance = balance + 100
WHERE id = 1;
DO SLEEP(8);
COMMIT;
END//
CREATE PROCEDURE run_timeout_demo()
BEGIN
DECLARE error_number INT DEFAULT 0;
DECLARE error_state CHAR(5) DEFAULT '00000';
DECLARE error_message TEXT DEFAULT '';
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION
BEGIN
GET DIAGNOSTICS CONDITION 1
error_number = MYSQL_ERRNO,
error_state = RETURNED_SQLSTATE,
error_message = MESSAGE_TEXT;
END;
SET SESSION innodb_lock_wait_timeout = 2;
START TRANSACTION;
UPDATE lock_timeout_demo
SET balance = balance + 10
WHERE id = 2;
UPDATE lock_timeout_demo
SET balance = balance + 10
WHERE id = 1;
SELECT error_number, error_state, error_message;
COMMIT;
END//
DELIMITER ;
DO SLEEP(3);
CALL run_timeout_demo();
DO SLEEP(6);
SELECT id, balance
FROM lock_timeout_demo
ORDER BY id;
DROP EVENT IF EXISTS ev_hold_lock;
DROP PROCEDURE IF EXISTS run_timeout_demo;
DROP TABLE lock_timeout_demo;
실행 결과(MySQL 8.0.x):
준비·정리 문장의 반복 출력은 줄이고, timeout과 최종 데이터 상태를 보여 주는 핵심 결과만 발췌했다.
mysql> CREATE TABLE lock_timeout_demo (...);
Query OK, 0 rows affected (0.01 sec)
mysql> INSERT INTO lock_timeout_demo (id, balance)
-> VALUES (1, 100), (2, 200);
Query OK, 2 rows affected (0.02 sec)
Records: 2 Duplicates: 0 Warnings: 0
mysql> CALL run_timeout_demo();
+--------------+-------------+--------------------------------------------------------+
| error_number | error_state | error_message |
+--------------+-------------+--------------------------------------------------------+
| 1205 | HY000 | Lock wait timeout exceeded; try restarting transaction |
+--------------+-------------+--------------------------------------------------------+
1 row in set (2.01 sec)
Query OK, 0 rows affected (2.01 sec)
mysql> SELECT id, balance
-> FROM lock_timeout_demo
-> ORDER BY id;
+----+---------+
| id | balance |
+----+---------+
| 1 | 200 |
| 2 | 210 |
+----+---------+
2 rows in set (0.00 sec)
최종값에서 id=1은 잠금 보유 이벤트의 변경만 반영되어 200이고, id=2는 timeout을 경험한 트랜잭션의 선행 변경이 반영되어 210이다. 즉 오류를 잡은 뒤 그대로 COMMIT한 예제는 부분 성공 위험을 의도적으로 보여 준다. 실제 애플리케이션은 이 경로를 따라서는 안 되며, 오류 직후 전체 트랜잭션을 롤백해야 한다.
4. 대기 관계를 Performance Schema로 진단하기
오류 로그에 1205가 남았다는 사실만으로 원인을 알 수는 없다. timeout이 발생하기 전에 performance_schema.data_lock_waits를 관찰하면 요청 잠금과 차단 잠금의 관계를 찾을 수 있다. data_locks를 두 번 조인하면 어느 테이블과 인덱스에서 어떤 lock mode가 충돌하는지 확인할 수 있다.
다음 재현에서는 첫 번째 이벤트가 행 잠금을 보유하고 두 번째 이벤트가 같은 행을 변경하려고 기다린다. 주 세션은 대기가 진행되는 동안 잠금 관계를 조회한다.
SET GLOBAL event_scheduler = ON;
DROP EVENT IF EXISTS ev_blocker;
DROP EVENT IF EXISTS ev_waiter;
DROP TABLE IF EXISTS lock_observe_demo;
CREATE TABLE lock_observe_demo (
id BIGINT PRIMARY KEY,
payload VARCHAR(30) NOT NULL
) ENGINE=InnoDB;
INSERT INTO lock_observe_demo VALUES (1, 'initial');
DELIMITER //
CREATE EVENT ev_blocker
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 SECOND
DO
BEGIN
START TRANSACTION;
UPDATE lock_observe_demo SET payload = 'blocker' WHERE id = 1;
DO SLEEP(8);
COMMIT;
END//
CREATE EVENT ev_waiter
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 2 SECOND
DO
BEGIN
SET SESSION innodb_lock_wait_timeout = 10;
START TRANSACTION;
UPDATE lock_observe_demo SET payload = 'waiter' WHERE id = 1;
COMMIT;
END//
DELIMITER ;
DO SLEEP(4);
SELECT req.OBJECT_SCHEMA,
req.OBJECT_NAME,
req.INDEX_NAME,
req.LOCK_TYPE AS waiting_lock_type,
req.LOCK_MODE AS waiting_lock_mode,
req.LOCK_STATUS AS waiting_status,
blk.LOCK_MODE AS blocking_lock_mode,
blk.LOCK_STATUS AS blocking_status
FROM performance_schema.data_lock_waits AS w
JOIN performance_schema.data_locks AS req
ON req.ENGINE_LOCK_ID = w.REQUESTING_ENGINE_LOCK_ID
AND req.ENGINE = w.ENGINE
JOIN performance_schema.data_locks AS blk
ON blk.ENGINE_LOCK_ID = w.BLOCKING_ENGINE_LOCK_ID
AND blk.ENGINE = w.ENGINE
WHERE req.OBJECT_SCHEMA = DATABASE()
AND req.OBJECT_NAME = 'lock_observe_demo';
DO SLEEP(7);
SELECT id, payload FROM lock_observe_demo;
DROP EVENT IF EXISTS ev_blocker;
DROP EVENT IF EXISTS ev_waiter;
DROP TABLE lock_observe_demo;
실행 결과(MySQL 8.0.x):
이 결과도 이벤트 생성과 대기용 SLEEP의 반복 출력은 줄이고, 실제 잠금 관계와 대기 해소 후 값을 발췌했다.
mysql> SELECT req.OBJECT_SCHEMA,
-> req.OBJECT_NAME,
-> req.INDEX_NAME,
-> req.LOCK_TYPE AS waiting_lock_type,
-> req.LOCK_MODE AS waiting_lock_mode,
-> req.LOCK_STATUS AS waiting_status,
-> blk.LOCK_MODE AS blocking_lock_mode,
-> blk.LOCK_STATUS AS blocking_status
-> FROM performance_schema.data_lock_waits AS w
-> ...
-> WHERE req.OBJECT_SCHEMA = DATABASE()
-> AND req.OBJECT_NAME = 'lock_observe_demo';
+-----------------+-------------------+------------+-------------------+-------------------+----------------+--------------------+-----------------+
| OBJECT_SCHEMA | OBJECT_NAME | INDEX_NAME | waiting_lock_type | waiting_lock_mode | waiting_status | blocking_lock_mode | blocking_status |
+-----------------+-------------------+------------+-------------------+-------------------+----------------+--------------------+-----------------+
| mysql_tech_note | lock_observe_demo | PRIMARY | RECORD | X,REC_NOT_GAP | WAITING | X,REC_NOT_GAP | GRANTED |
+-----------------+-------------------+------------+-------------------+-------------------+----------------+--------------------+-----------------+
1 row in set (0.00 sec)
mysql> SELECT id, payload FROM lock_observe_demo;
+----+---------+
| id | payload |
+----+---------+
| 1 | waiter |
+----+---------+
1 row in set (0.00 sec)
검증 환경에서는 대기 잠금이 X,REC_NOT_GAP WAITING, 차단 잠금이 X,REC_NOT_GAP GRANTED로 관찰된다. LOCK_MODE, LOCK_DATA, 스레드 식별자 같은 세부값은 MySQL minor version과 실행 경로에 따라 달라질 수 있으므로, 운영에서는 고정 문자열보다 요청자와 차단자의 관계, 대상 인덱스, 대기 지속 시간을 함께 해석한다.
4.1 운영 진단 쿼리에서 함께 볼 정보
실제 장애에서는 다음 순서로 범위를 좁힌다.
data_lock_waits에서 requesting/blocking 관계를 찾는다.data_locks에서 대상 schema, table, index와 lock mode를 확인한다.performance_schema.threads로 내부THREAD_ID와 애플리케이션 연결의PROCESSLIST_ID를 연결한다.- statement instrumentation이 활성화되어 있다면
events_statements_current또는 statement history에서 실행 SQL을 확인한다. information_schema.INNODB_TRX에서 트랜잭션 시작 시각, 상태, 변경 행 수를 함께 본다.- 차단 세션을 종료하기 전에 업무 영향, 커밋 가능성, 복제·재시도 부작용을 확인한다.
SQL 텍스트나 bind 값에는 개인정보와 업무 데이터가 포함될 수 있다. 진단 결과를 티켓이나 채팅에 복사할 때는 값을 마스킹하고 접근 권한을 제한해야 한다.
5. timeout 값을 정하는 운영 기준
innodb_lock_wait_timeout에는 모든 시스템에 맞는 단일 정답이 없다. 너무 길면 대기 요청이 쌓여 장애가 확산되고, 너무 짧으면 정상적인 순간 경합까지 실패로 바꾼다. 값은 애플리케이션 요청 제한 시간과 트랜잭션 서비스 수준 목표 안에서 결정해야 한다.
예를 들어 HTTP 요청 제한 시간이 5초인데 DB 잠금 대기를 50초로 두면, 클라이언트 요청이 이미 취소된 뒤에도 DB 작업이 계속 대기할 수 있다. 반대로 배치 작업이 수십 초 동안 큰 범위를 갱신하는 시스템에서 1초를 일괄 적용하면 재시도 폭풍이 발생할 수 있다.
다음 관계를 기준으로 검토한다.
잠금 대기 한도
< 애플리케이션의 DB 호출 제한 시간
< 전체 요청 제한 시간
단, 단순히 숫자만 줄이기 전에 다음 원인을 먼저 개선한다.
- 트랜잭션 안에서 외부 API 호출이나 사용자 입력을 기다리는 코드
- 동일한 업무 객체를 서로 다른 순서로 잠그는 코드 경로
- 조건 인덱스 부재로 필요 이상 범위를 스캔하고 잠그는
UPDATE또는DELETE - 한 트랜잭션에서 지나치게 많은 행을 처리하는 배치
- 커밋 직전의 긴 계산, 파일 처리, 네트워크 작업
- connection pool에서
autocommit또는 미완료 트랜잭션 상태가 반환되는 문제
업무 유형이 다르면 세션값도 구분할 수 있다. 짧은 OLTP 요청에는 비교적 짧은 한도를 적용하고, 의도적으로 긴 정비 작업에는 별도 연결과 명확한 작업 창, 모니터링, 취소 기준을 적용한다. 전역값 하나로 모든 워크로드를 맞추려 하지 않는 편이 안전하다.
6. 애플리케이션 재시도 정책
6.1 문장이 아니라 업무 트랜잭션 전체를 재시도한다
안전한 재시도 단위는 BEGIN부터 COMMIT까지의 논리적 업무 단위다. 오류 1205 또는 1213을 받으면 현재 트랜잭션을 먼저 ROLLBACK하고, 가능하면 같은 연결의 세션 상태를 초기화한 뒤 새 트랜잭션으로 처음부터 실행한다.
function execute_business_transaction(input):
max_attempts = 3
for attempt in 1..max_attempts:
begin transaction
try:
validate current database state
apply every change in deterministic lock order
commit
return success
catch database_error as e:
rollback
if e.mysql_errno not in [1205, 1213]:
raise e
if attempt == max_attempts:
raise retry_exhausted
sleep(exponential_backoff(attempt) + random_jitter())
오류 문자열을 부분 일치로 분류하기보다 드라이버가 제공하는 MySQL error number를 사용한다. SQLSTATE HY000은 범용 상태이므로 1205 식별에는 충분히 구체적이지 않다.
6.2 bounded exponential backoff와 jitter를 적용한다
잠금이 풀리자마자 모든 실패 요청이 동시에 재시도하면 같은 hot row에서 다시 충돌한다. 재시도 횟수에 따라 지연을 늘리고 작은 난수를 더해 요청을 분산한다. 예를 들어 첫 재시도는 수십 밀리초, 다음 재시도는 수백 밀리초 범위로 늘릴 수 있지만, 실제 값은 요청 SLO와 충돌 지속 시간 분포를 측정해 정해야 한다.
재시도에는 반드시 다음 상한이 있어야 한다.
- 최대 시도 횟수
- 전체 경과 시간 제한
- 요청 deadline 초과 시 즉시 중단
- retry budget과 동시 재시도 수 제한
- 반복 실패 시 circuit breaker 또는 상위 큐로 이관
6.3 멱등성과 커밋 결과 불확실성을 분리한다
1205를 받은 문장은 서버가 실패를 명시했지만, 네트워크 단절이나 writer failover가 COMMIT 응답 시점에 발생하면 커밋 성공 여부가 불확실할 수 있다. 이 상황까지 동일한 단순 재시도로 처리하면 주문, 결제, 메시지 발행이 중복될 수 있다.
업무 요청 식별자를 unique key로 저장하거나, 상태 전이를 조건부 UPDATE로 표현하고, outbox 패턴으로 외부 부작용을 커밋과 연결하는 방식이 필요하다. “SQL이 일시 오류면 세 번 반복한다”는 구현만으로는 안전한 재시도 정책이 완성되지 않는다.
6.4 재시도 가능한 오류의 범위를 좁힌다
오류 1205와 1213도 항상 성공 가능성을 뜻하지 않는다. 다음 조건이면 즉시 반복보다 원인 제거 또는 상위 계층 실패 처리가 적절하다.
- 동일한 업무 키에서 시도할 때마다 timeout이 반복된다.
- 남은 요청 deadline이 다음 backoff보다 짧다.
- 트랜잭션에 멱등하지 않은 외부 부작용이 포함되어 있다.
- DDL 또는 운영 작업과 충돌해 Metadata Lock이 누적되고 있다.
- 재시도율이 임계치를 넘어 시스템 부하를 더 높이고 있다.
7. Aurora MySQL에서의 운영 해석
Aurora MySQL도 MySQL 호환 InnoDB 잠금과 innodb_lock_wait_timeout의 기본 의미를 따른다. 다만 설정은 일반적으로 DB parameter group과 세션 설정의 관계를 확인해야 하며, 적용 가능 범위와 재부팅 필요 여부는 사용하는 Aurora MySQL 버전의 파라미터 메타데이터로 검증해야 한다.
Aurora 운영에서는 다음 차이를 함께 고려한다.
- 쓰기 트랜잭션의 잠금 경합은 writer에서 발생한다. reader endpoint의 읽기 지연 문제와 구분한다.
- writer failover는 연결 단절과 커밋 결과 불확실성을 만들 수 있다. 1205 재시도와 failover 재연결 정책을 같은 오류 한 종류로 뭉치지 않는다.
- Performance Insights 또는 Database Insights에서 DB load와 wait 차원을 보고, 애플리케이션의 1205 비율과 함께 시간축을 맞춘다.
- parameter group을 바꿨더라도 기존 connection pool 세션값이 즉시 같은 값이 되었다고 가정하지 않는다.
- timeout 값을 늘려 증상을 가리기보다 hot key, 긴 트랜잭션, 쿼리 계획, writer 부하를 먼저 분석한다.
스토리지가 분산되어 있다는 사실은 논리적인 행 잠금 경합을 제거하지 않는다. Aurora의 빠른 failover나 스토리지 구조와 별개로, 한 writer에서 같은 레코드를 갱신하는 트랜잭션은 여전히 올바른 잠금 순서와 짧은 트랜잭션 경계를 필요로 한다.
8. 흔한 오해와 실패 패턴
8.1 timeout 값을 늘리면 문제가 해결된다
값을 늘리면 오류 발생 시점만 늦어질 수 있다. 차단 트랜잭션이 1분 동안 외부 호출을 기다린다면, 대기 한도를 50초에서 120초로 늘리는 것은 연결 누적 시간을 늘리는 조치가 될 수 있다.
8.2 실패한 UPDATE만 다시 실행하면 된다
기본 동작에서 선행 문장이 남을 수 있기 때문에, 단일 문장 재시도는 업무 불변식을 깨뜨릴 수 있다. 전체 ROLLBACK 후 트랜잭션을 다시 시작해야 한다.
8.3 재시도 횟수만 넣으면 안전하다
backoff와 jitter가 없으면 동시 요청이 같은 시점에 다시 충돌한다. 멱등성이나 요청 deadline이 없으면 중복 반영 또는 지연 증폭이 발생한다.
8.4 1205는 모두 행 잠금 문제다
오류 번호와 실제 대기 대상을 함께 확인해야 한다. Metadata Lock, 애플리케이션 pool 고갈, 느린 쿼리, 네트워크 timeout은 관측 지점과 해결 방법이 다르다.
8.5 차단 세션은 즉시 KILL하면 된다
차단 세션이 중요한 대량 변경을 커밋하기 직전일 수 있다. 무조건 종료하면 rollback이 더 오래 걸리고 부하가 커질 수 있다. 트랜잭션 시작 시각, 변경량, 업무 소유자, 복구 비용을 확인한 뒤 종료 여부를 결정한다.
9. 운영 체크리스트
설정과 연결
- 애플리케이션 실제 세션의
@@SESSION.innodb_lock_wait_timeout
트랜잭션 설계
- 오류 1205와 1213에서 현재 트랜잭션 전체를
ROLLBACK
재시도 안전성
-
COMMIT
장애 대응
-
data_lock_waits와data_locks
10. 결론
innodb_lock_wait_timeout은 잠금 경합을 없애는 설정이 아니라, 무한정 기다리지 않도록 실패 경계를 만드는 설정이다. 오류 1205의 기본 동작이 문장 단위 롤백이라는 점을 이해해야 부분 커밋을 피할 수 있다. 애플리케이션은 현재 트랜잭션 전체를 롤백하고, 멱등성을 확보한 업무 단위를 제한된 backoff 정책으로 다시 실행해야 한다.
운영자는 timeout 횟수만 보지 말고 요청 잠금과 차단 잠금, 대상 인덱스, 트랜잭션 수명, 재시도 증폭을 함께 관찰해야 한다. 다음 잠금 진단 주제에서는 deadlock graph와 SHOW ENGINE INNODB STATUS, Performance Schema 정보를 결합해 오류 1213의 희생 트랜잭션과 잠금 순서를 해석하는 방법을 다룬다.