금요일 저녁, 정산 배치가 돌아가는 시간이었다. 평소엔 1분이면 끝나던 작업이 멈춰 있었고, 화면에는 낯익은 한 줄이 떠 있었다. Lock wait timeout exceeded; try restarting transaction. 누군가는 이 메시지를 보면 일단 서버부터 재시작하고 보지만, 그건 대개 문제를 미루는 일일 뿐이다. 오늘은 이 에러가 생기는지, 그리고 현장에서 어떻게 차분히 풀어가는지 정리해 본다.

에러가 말하는 진짜 뜻

이 메시지는 "내 트랜잭션이 어떤 행(row)에 잠금을 걸려고 기다렸는데, 정해진 시간 안에 그 잠금을 얻지 못했다"는 뜻이다. MySQL(InnoDB)은 여러 트랜잭션이 같은 데이터를 동시에 건드릴 때 충돌을 막으려고 행 단위 잠금을 건다. 문제는 한쪽이 잠금을 오래 쥐고 놓지 않을 때다.

기본 대기 시간은 innodb_lock_wait_timeout으로, 보통 50초로 잡혀 있다. 50초를 기다렸는데도 못 얻으면 기다리던 쪽이 포기하고 위 에러를 던진다.

에러를 던진 트랜잭션이 범인인 경우는 드물다. 진짜 원인은 잠금을 오래 붙잡고 있는 다른 누군가다.

범인을 찾는 순서

추측 대신 데이터를 본다. MySQL 8.x에는 잠금 상황을 그대로 보여주는 시스템 뷰가 있다. 먼저 지금 잠금을 기다리는 관계부터 확인한다.

SELECT * FROM performance_schema.data_lock_waits\G

SELECT thread_id, processlist_id, processlist_time, processlist_info
FROM performance_schema.threads
WHERE processlist_time > 5
ORDER BY processlist_time DESC;

오래 살아 있는 세션이 보이면, 그게 잠금을 쥔 채 커밋을 안 하고 있는 트랜잭션일 가능성이 높다. 더 직접적으로는 아래 쿼리가 "누가 누구를 막고 있는지"를 한눈에 보여준다.

SELECT
  r.trx_id              AS 기다리는_트랜잭션,
  r.trx_mysql_thread_id AS 기다리는_세션,
  b.trx_id              AS 막고있는_트랜잭션,
  b.trx_mysql_thread_id AS 막고있는_세션,
  b.trx_query           AS 막고있는_쿼리
FROM information_schema.innodb_lock_waits w
JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id;

막고있는_세션 번호가 나오면, 급한 불은 그 세션을 끊어 끌 수 있다. KILL 1234; 한 줄이면 된다. 다만 이건 응급처치다. 왜 그 세션이 잠금을 오래 쥐고 있었는지를 풀지 않으면 다음 주에 똑같은 화면을 또 본다.

자주 보이는 세 가지 원인

첫째는 커밋을 잊은 긴 트랜잭션이다. 애플리케이션에서 BEGIN을 해 놓고 중간에 외부 API를 호출하거나 사용자 입력을 기다리는 구조라면, 그 사이 잠금이 계속 살아 있다. 트랜잭션은 가능한 한 짧게, 외부 호출은 트랜잭션 바깥으로 빼는 게 원칙이다.

둘째는 인덱스 없는 UPDATE/DELETE다. 조건 컬럼에 인덱스가 없으면 InnoDB가 더 많은 행에 잠금을 걸어, 무관해 보이는 다른 쿼리까지 줄줄이 막는다. 아래처럼 실행계획을 확인해 typeALL(풀 스캔)로 나오면 인덱스를 의심한다.

EXPLAIN UPDATE orders SET status = 'done' WHERE buyer_id = 1024;
증상흔한 원인먼저 볼 곳
특정 시각에만 발생배치·정산 작업 겹침크론 스케줄
점점 느려지다 폭발커넥션 누수·미커밋애플리케이션 트랜잭션 코드
무작위로 발생인덱스 부재 풀스캔EXPLAIN 실행계획

셋째는 교착(데드락)과의 혼동이다. 데드락은 서로가 서로를 막아 즉시 한쪽을 죽이는 별개 에러(Deadlock found)다. 타임아웃은 그보다 느긋한, "기다리다 지친" 상황이라는 점을 구분하면 진단이 빨라진다.

다시 안 보려면

응급처치로 세션을 끊었다면, 그다음은 재발 방지다. 트랜잭션 경계를 좁히고, 자주 갱신되는 테이블의 조건 컬럼에 인덱스를 확인하고, 정산·집계 같은 무거운 작업은 사용자 트래픽이 적은 시간대로 옮긴다. 모니터링에 "5초 이상 살아 있는 트랜잭션" 알림을 하나 걸어 두면, 폭발하기 전에 손쓸 시간을 벌 수 있다.

에러 메시지는 야속하게 느껴지지만, 사실은 친절한 신호다. "어딘가에서 누군가 너무 오래 붙잡고 있어요"라고 알려 주는 것이니까. 다음에 같은 화면을 만나면 서버 재시작 버튼으로 손이 가기 전에, 위의 잠금 조회 쿼리부터 한 번 던져 보시길. 범인은 대개 금방 잡힌다.