MySQL 커밋을 잊은 세션 찾기: INNODB_TRX로 장기 트랜잭션 추적

장애 대응 중에 SHOW PROCESSLIST를 열어 보면 대부분의 연결이 Sleep입니다. 실행 중인 SQL이 없으니 문제없는 연결로 넘기기 쉽습니다. 그런데 Sleep 연결 중 하나가 UPDATE를 실행한 뒤 COMMIT을 하지 않았다면, 그 연결은 아무 SQL도 실행하지 않는 상태로 행 잠금을 계속 쥐고 있습니다.
이 글에서는 MySQL 8.4.11에서 커밋을 빠뜨린 세션을 직접 만들고, information_schema.INNODB_TRX와 Performance Schema로 그 세션을 찾아냈습니다. 확인한 내용은 다음 세 가지입니다.
- 연결 상태(Sleep)와 트랜잭션 상태(RUNNING)가 왜 동시에 나타나는지
- 현재 SQL이
NULL인데 잠금은 어떻게 남아 있는지 - ROLLBACK 후 무엇을 확인해야 정리가 끝났다고 말할 수 있는지
관측 시점의 트랜잭션 경과 시간은 20초였습니다. 운영 장애 사례가 아니라 커밋 누락을 짧게 재현한 실험이며, 20초를 장기 트랜잭션의 기준으로 제시하려는 것은 아닙니다.
테스트 환경과 데이터 구성
| 항목 | 실험 조건 |
|---|---|
| MySQL 버전 | 8.4.11 (Community, Windows) |
| 스키마·테이블 | lab_trx23.accounts |
| 스토리지 엔진 | InnoDB |
| 테스트 데이터 | 3행 (account_id=1의 초기 잔액 1000.00) |
| 인덱스 | PRIMARY KEY (account_id) |
| 격리 수준 | REPEATABLE-READ |
| 자동 커밋 | autocommit=1 |
| Performance Schema | 활성화 |
| 관측 대상 연결 | 세션 A, 연결 번호 69 |
| 측정 방식 | 세션 A에서 수정, 세션 B에서 관측 |
세션 B는 autocommit=1을 유지했고 명시적 트랜잭션을 시작하지 않았습니다. 관측하는 쪽이 트랜잭션을 열면 그 자체가 INNODB_TRX에 잡히기 때문입니다. 세션 A는 UPDATE 후 추가 SQL 없이 연결만 유지했습니다. 처리 속도를 보는 실험이 아니므로 실행계획이나 캐시 상태는 비교하지 않았습니다.
준비 SQL은 해당 스키마가 없는 실험 환경에서 한 번 실행하는 구성입니다.
CREATE DATABASE lab_trx23 CHARACTER SET utf8mb4;
USE lab_trx23;
CREATE TABLE accounts (
account_id INT NOT NULL,
owner_name VARCHAR(30) NOT NULL,
balance DECIMAL(12,2) NOT NULL,
PRIMARY KEY (account_id)
) ENGINE = InnoDB;
INSERT INTO accounts VALUES
(1, 'customer_a', 1000.00),
(2, 'customer_b', 2000.00),
(3, 'customer_c', 3000.00);
COMMIT;
실험 전에 세션 A에서 버전, 연결 번호, 격리 수준, Performance Schema 활성화 여부, 데이터 건수와 인덱스를 한 번에 확인했습니다. 이후 결과를 해석할 때 전제가 되는 값이므로 먼저 기록해 두었습니다.
SELECT
VERSION() AS mysql_version,
CONNECTION_ID() AS a_connection_id,
@@session.autocommit AS autocommit_mode,
@@session.transaction_isolation AS isolation_level,
@@performance_schema AS ps_enabled,
(
SELECT COUNT(*)
FROM lab_trx23.accounts
) AS test_rows,
(
SELECT balance
FROM lab_trx23.accounts
WHERE account_id = 1
) AS initial_balance,
(
SELECT GROUP_CONCAT(
CONCAT(INDEX_NAME, ':', COLUMN_NAME)
ORDER BY INDEX_NAME, SEQ_IN_INDEX
SEPARATOR ', '
)
FROM information_schema.statistics
WHERE TABLE_SCHEMA = 'lab_trx23'
AND TABLE_NAME = 'accounts'
) AS index_columns;

버전 8.4.11, 연결 번호 69, autocommit 1(Workbench에서는 true로 표시), REPEATABLE-READ, Performance Schema 활성, 3행, 초기 잔액 1000.00, PRIMARY 인덱스를 확인했습니다. 이후 결과는 모두 이 조건을 기준으로 해석합니다.
세션 A: UPDATE 후 COMMIT을 생략
SET SESSION autocommit = 1;
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
UPDATE lab_trx23.accounts
SET balance = balance - 100
WHERE account_id = 1;
SELECT
CONNECTION_ID() AS a_connection_id,
@@session.autocommit AS autocommit_mode,
account_id,
owner_name,
balance AS a_visible_balance
FROM lab_trx23.accounts
WHERE account_id = 1;

세션 A에서는 잔액이 900.00으로 보입니다. 같은 트랜잭션 안에서는 자신이 변경한 값을 읽을 수 있기 때문입니다. 그래서 수정한 세션에서 바뀐 값이 보인다는 사실은 커밋 여부를 알려주지 않습니다. 개발자가 “저장은 됐는데 다른 화면에서는 안 보인다”고 말하는 상황이 바로 이 지점에서 생깁니다.
autocommit_mode 값이 1(Workbench에서는 true로 표시)이라는 점도 주의해야 합니다. autocommit은 문장 단위로 자동 커밋할지 정하는 설정일 뿐입니다. START TRANSACTION으로 명시적 트랜잭션을 시작하면 autocommit=1이어도 COMMIT이나 ROLLBACK을 직접 실행해야 끝납니다. @@autocommit 값만 보고 “이 세션에는 열린 트랜잭션이 없다”고 판단할 수 없습니다.
가설: 실행 중인 SQL이 아니라 끝나지 않은 트랜잭션
이 상황에서 처음 떠올리기 쉬운 가설은 “UPDATE가 오래 걸리고 있다”는 것입니다. 하지만 UPDATE는 이미 끝났고 연결은 Sleep입니다. Sleep은 클라이언트가 다음 명령을 보내기를 기다리는 상태일 뿐이며, 트랜잭션이 끝났는지에 대해서는 아무것도 알려주지 않습니다.
그래서 SQL 실행 상태와 트랜잭션 생존 여부를 분리해 다음 세 가지를 확인했습니다.
- 연결 69가
INNODB_TRX에 열린 트랜잭션으로 남아 있는가 - 현재 SQL이 없어도 변경 행 수와 잠금이 유지되는가
- 원래 연결에서 ROLLBACK하면 연결은 남고 트랜잭션과 잠금만 사라지는가
세션 B: INNODB_TRX로 열린 트랜잭션 확인
관측 대상 연결 번호 지정
세션 B는 세션 A와 다른 연결입니다. 이번 실험에서 A의 번호는 69였습니다. 재현할 때는 위의 환경 확인 단계에서 얻은 본인 환경의 연결 번호로 바꿔야 합니다.
SET SESSION autocommit = 1;
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET @a_id = 69;
SELECT
CONNECTION_ID() AS b_connection_id,
@a_id AS target_a_id;
트랜잭션 진단 SQL
INNODB_TRX는 InnoDB 기준의 트랜잭션 정보를, performance_schema.threads는 연결 상태를 보여 줍니다. 두 테이블을 연결 번호로 묶으면 “연결은 쉬고 있는데 트랜잭션은 살아 있는” 상태를 한 행에서 볼 수 있습니다.
SELECT
trx.TRX_MYSQL_THREAD_ID AS a_connection_id,
th.PROCESSLIST_COMMAND AS session_command,
th.PROCESSLIST_TIME AS command_age_sec,
trx.TRX_STATE AS trx_state,
trx.TRX_STARTED AS trx_started,
TIMESTAMPDIFF(
SECOND, trx.TRX_STARTED, NOW()
) AS trx_age_sec,
trx.TRX_ROWS_MODIFIED AS rows_modified,
trx.TRX_ROWS_LOCKED AS rows_locked_approx,
trx.TRX_QUERY AS current_trx_sql
FROM information_schema.innodb_trx AS trx
LEFT JOIN performance_schema.threads AS th
ON th.PROCESSLIST_ID = trx.TRX_MYSQL_THREAD_ID
WHERE trx.TRX_MYSQL_THREAD_ID = @a_id;
INNODB_TRX를 조회하려면 PROCESS 권한이 필요하고, Performance Schema 테이블에도 SELECT 권한이 있어야 합니다. 모니터링 계정을 따로 만들 때 권한이 빠지면 기대한 행이 보이지 않을 수 있으므로, 결과가 비어 있다면 권한부터 확인하는 것이 좋습니다.

| 관측 항목 | 실제 결과 | 해석 |
|---|---|---|
| session_command | Sleep | 다음 명령을 기다리는 연결 |
| command_age_sec | 20 | Sleep 상태가 지속된 시간 |
| trx_state | RUNNING | 트랜잭션이 아직 열려 있음 |
| trx_age_sec | 20 | 트랜잭션 시작 이후 경과 시간 |
| rows_modified | 1 | 변경된 행이 존재함 |
| rows_locked_approx | 1 | 잠금 행 수(근삿값) |
| current_trx_sql | NULL | 지금 실행 중인 SQL이 없음 |
이 결과에서 자주 틀리는 해석
이 화면에는 잘못 읽기 쉬운 값이 세 군데 있습니다.
| 잘못된 해석 | 실제 의미 |
|---|---|
| RUNNING이니 UPDATE가 20초째 실행 중이다 | RUNNING은 트랜잭션이 열려 있다는 뜻입니다. SQL은 이미 끝났습니다. |
| command_age_sec와 trx_age_sec가 같으니 같은 지표다 | 이번에는 UPDATE 직후 Sleep이 시작되어 값이 같았을 뿐입니다. 트랜잭션 안에서 SQL을 하나 더 실행하면 command_age_sec는 다시 0부터 세지만 trx_age_sec는 계속 늘어납니다. |
| TRX_QUERY가 NULL이니 아무 작업도 안 했다 | TRX_QUERY는 지금 실행 중인 문장만 보여 줍니다. 앞서 실행한 UPDATE는 남지 않으며, rows_modified=1이 실제 변경이 있었다는 증거입니다. |
운영에서 Sleep 경과 시간(PROCESSLIST_TIME)만으로 장기 트랜잭션을 찾으면 두 번째 함정에 걸립니다. 애플리케이션이 트랜잭션 안에서 가벼운 SELECT를 주기적으로 실행하면 Sleep 시간은 계속 짧게 유지되기 때문입니다. 트랜잭션의 나이는 반드시 TRX_STARTED 기준으로 계산해야 합니다.
잠금은 남아 있고, 다른 세션에는 원래 값이 보인다
세션 B가 읽는 잔액과 세션 A가 보유한 잠금을 한 번에 확인했습니다.
SELECT
trx.TRX_MYSQL_THREAD_ID AS a_connection_id,
(
SELECT balance
FROM lab_trx23.accounts
WHERE account_id = 1
) AS b_visible_balance,
dl.OBJECT_NAME AS locked_table,
dl.INDEX_NAME AS index_name,
dl.LOCK_TYPE AS lock_type,
dl.LOCK_MODE AS lock_mode,
dl.LOCK_STATUS AS lock_status,
dl.LOCK_DATA AS lock_data
FROM information_schema.innodb_trx AS trx
JOIN performance_schema.data_locks AS dl
ON dl.ENGINE_TRANSACTION_ID = trx.TRX_ID
AND dl.ENGINE = 'INNODB'
WHERE trx.TRX_MYSQL_THREAD_ID = @a_id
AND dl.OBJECT_SCHEMA = 'lab_trx23'
AND dl.OBJECT_NAME = 'accounts'
ORDER BY dl.LOCK_TYPE, dl.INDEX_NAME;

| 대상 | 잠금 유형 | 모드 | 상태 | LOCK_DATA |
|---|---|---|---|---|
| accounts의 PRIMARY | RECORD | X,REC_NOT_GAP | GRANTED | 1 |
| accounts 테이블 | TABLE | IX | GRANTED | NULL |
잠금 두 줄의 의미
X,REC_NOT_GAP은 기본 키 값 1인 레코드 한 건에 대한 배타적 잠금이며, 갭 잠금은 포함되지 않았습니다. 존재하는 기본 키 한 건을 동등 조건으로 수정했으니 예상과 일치합니다. 같은 UPDATE라도 WHERE 조건이 범위 검색이거나 인덱스를 타지 않으면 잠금 범위가 크게 달라지므로, 운영에서는 이 LOCK_MODE 값을 먼저 확인하는 것이 좋습니다.
IX는 “이 테이블 안의 어떤 행에 배타적 잠금을 걸겠다”는 의도를 나타내는 테이블 수준 잠금입니다. 테이블 전체를 막는 잠금이 아니므로 다른 세션의 일반 조회나 다른 행 수정은 막지 않습니다. 잠금 목록이 두 줄이라고 데이터 두 행이 잠긴 것도 아닙니다.
행 잠금보다 운영에서 더 크게 문제가 되는 것은 메타데이터 잠금입니다. 트랜잭션이 테이블에 접근하면 트랜잭션이 끝날 때까지 해당 테이블의 메타데이터 잠금(MDL)을 유지합니다. 이 상태에서 다른 세션이 ALTER TABLE을 실행하면 Waiting for table metadata lock 상태로 대기하고, 그 뒤에 들어온 일반 SELECT까지 ALTER 뒤에 줄을 서게 됩니다. 배포 중 스키마 변경이 멈추고 서비스 조회까지 막혔다면 커밋을 빠뜨린 Sleep 세션을 먼저 의심해야 합니다. 이번 실험에서는 DDL 대기를 재현하지 않았으며, 실제 상황에서는 performance_schema.metadata_locks에서 OWNER_THREAD_ID로 잠금 소유자를 확인할 수 있습니다.
세션 B가 1000을 읽은 이유
세션 B의 일반 SELECT는 잠금 없는 일관된 읽기이므로, 커밋되지 않은 변경 대신 커밋된 버전(1000.00)을 읽습니다. 수정한 세션에서는 900, 다른 세션에서는 1000이 보이는 것은 데이터 오류가 아니라 정상 동작입니다.
다만 이번 실험에서는 다른 세션의 UPDATE나 SELECT ... FOR UPDATE로 대기를 일으키지 않았습니다. GRANTED는 A가 잠금을 획득했다는 뜻이지, 다른 세션이 실제로 차단되었다는 뜻은 아닙니다. 만약 다른 세션이 같은 행을 수정하려 했다면 innodb_lock_wait_timeout(기본 50초)만큼 기다리다 오류로 끝났을 것입니다. 이때 오류가 나는 쪽은 잠금을 쥔 A가 아니라 기다린 세션입니다. 장애 보고에서 타임아웃이 난 세션만 보고 원인을 찾으면 진짜 원인인 Sleep 세션을 놓치는 이유가 여기에 있습니다.
ROLLBACK 전후 비교: 연결은 남고 트랜잭션만 사라진다
세션 A에서 실험용 변경을 취소했습니다.
ROLLBACK;
이 명령은 트랜잭션을 소유한 A 연결에서 실행해야 합니다. 세션 B에서 ROLLBACK을 실행하면 B 자신에게만 적용되고 A의 트랜잭션은 그대로 남습니다.
A 연결을 유지한 채 세션 B에서 연결, 트랜잭션, 잠금, 잔액을 한 번에 확인했습니다.
SELECT
@a_id AS a_connection_id,
(
SELECT COUNT(*)
FROM performance_schema.threads
WHERE PROCESSLIST_ID = @a_id
) AS a_connection_exists,
(
SELECT COUNT(*)
FROM information_schema.innodb_trx
WHERE TRX_MYSQL_THREAD_ID = @a_id
) AS a_open_trx_count,
(
SELECT COUNT(*)
FROM performance_schema.data_locks AS dl
JOIN performance_schema.threads AS th
ON th.THREAD_ID = dl.THREAD_ID
WHERE th.PROCESSLIST_ID = @a_id
AND dl.ENGINE = 'INNODB'
AND dl.OBJECT_SCHEMA = 'lab_trx23'
AND dl.OBJECT_NAME = 'accounts'
) AS a_test_lock_count,
(
SELECT balance
FROM lab_trx23.accounts
WHERE account_id = 1
) AS balance_after_rollback;

| 확인 항목 | ROLLBACK 이전 | ROLLBACK 이후 |
|---|---|---|
| A 연결 존재 | 존재 | 존재 (1) |
| A의 열린 InnoDB 트랜잭션 | 1건 | 0건 |
| A의 테스트 테이블 잠금 항목 | 2개 | 0개 |
| B에서 보이는 잔액 | 1000.00 | 1000.00 |
눈여겨볼 행은 마지막 줄입니다. 세션 B의 잔액은 ROLLBACK 전후 모두 1000입니다. 미커밋 상태에서도 B에는 원래 값이 보였으므로, 잔액 조회만으로는 트랜잭션이 정리됐는지 판단할 수 없습니다. 정리 여부의 근거는 “연결은 남아 있는데 INNODB_TRX와 data_locks 항목이 0이 됐다”는 두 숫자의 조합입니다.
또 하나 확인된 점은 문제를 해결하려고 연결을 끊을 필요가 없었다는 것입니다. 커넥션 풀 환경에서는 연결을 끊는 것보다 원래 연결에서 트랜잭션을 정상 종료하는 편이 애플리케이션에 주는 영향이 작습니다.
운영에서 커밋 누락 세션을 다루는 기준
1. 오래된 트랜잭션부터 후보로 조회
운영에서는 연결 번호를 모르는 경우가 대부분이므로, 열린 트랜잭션을 시작 시각 순으로 정렬해 후보를 찾습니다.
SELECT
trx.TRX_MYSQL_THREAD_ID AS connection_id,
th.PROCESSLIST_USER AS session_user,
th.PROCESSLIST_HOST AS client_host,
th.PROCESSLIST_COMMAND AS session_command,
TIMESTAMPDIFF(
SECOND, trx.TRX_STARTED, NOW()
) AS trx_age_sec,
trx.TRX_STATE,
trx.TRX_ROWS_MODIFIED,
trx.TRX_QUERY
FROM information_schema.innodb_trx AS trx
LEFT JOIN performance_schema.threads AS th
ON th.PROCESSLIST_ID = trx.TRX_MYSQL_THREAD_ID
ORDER BY trx.TRX_STARTED
LIMIT 20;
이 결과는 후보 목록일 뿐입니다. 정상적인 배치 작업도 긴 트랜잭션을 사용하므로 경과 시간만으로 커밋 누락이라고 단정하지 않습니다. 다음 조건이 겹칠 때 커밋 누락을 강하게 의심할 수 있습니다.
- session_command가 Sleep이고 TRX_QUERY가 NULL
- TRX_ROWS_MODIFIED가 0보다 큼 (실제로 무언가를 바꿨음)
- 해당 애플리케이션의 평소 트랜잭션 처리 시간보다 훨씬 김
- 같은 사용자·호스트에서 반복적으로 나타남
2. 잠금 보유와 실제 차단을 구분
data_locks는 보유 중이거나 요청 중인 잠금을 보여 줍니다. 어느 세션이 어느 세션을 기다리게 하는지는 performance_schema.data_lock_waits나 이를 정리한 sys.innodb_lock_waits 뷰로 따로 조사해야 합니다. 대기 중인 세션이 없다면 그 Sleep 세션은 아직 서비스에 영향을 주지 않는 잠재 위험으로 분류할 수 있습니다.
3. 마지막으로 실행된 SQL은 다른 곳에서 찾기
TRX_QUERY가 NULL이면 그 트랜잭션이 무엇을 바꿨는지 INNODB_TRX만으로는 알 수 없습니다. 이때는 Performance Schema의 문장 이력을 연결 번호로 조회해 볼 수 있습니다. 아래 SQL은 이번 실험에서 실행하지 않았으며, events_statements_history consumer가 켜져 있어야 결과가 나옵니다.
SELECT
h.EVENT_ID,
h.SQL_TEXT,
h.ROWS_AFFECTED
FROM performance_schema.events_statements_history AS h
JOIN performance_schema.threads AS th
ON th.THREAD_ID = h.THREAD_ID
WHERE th.PROCESSLIST_ID = @a_id
ORDER BY h.EVENT_ID;
이 이력은 스레드당 최근 몇 건만 보관합니다. 트랜잭션 시작 후 다른 SQL이 많이 실행됐다면 원인이 된 UPDATE는 이미 밀려났을 수 있으므로 애플리케이션 로그를 함께 확인하는 것이 확실합니다.
4. 조치 순서: 소유자 확인 → 정상 종료 → 강제 종료
변경을 확정할지 취소할지는 업무 판단이 필요합니다. 먼저 해당 연결의 사용자와 호스트로 애플리케이션 담당자를 찾아, 원래 연결에서 COMMIT 또는 ROLLBACK하도록 하는 것이 원칙입니다. Sleep이라는 이유만으로 연결을 일괄 종료하지 않습니다.
강제 조치가 필요할 때는 두 명령의 차이를 알아야 합니다.
| 명령 | 동작 | Sleep 커밋 누락 세션에 대한 효과 |
|---|---|---|
KILL QUERY 69 | 실행 중인 문장만 중단 | 실행 중인 문장이 없으므로 트랜잭션과 잠금이 그대로 남음 |
KILL 69 | 연결 종료 | 열린 트랜잭션이 롤백되며 잠금이 해제됨 |
KILL로 연결을 끊으면 InnoDB가 롤백을 진행하며, 변경량이 크면 롤백에도 상당한 시간이 걸릴 수 있습니다. 조치 직후에는 위의 ROLLBACK 이후 확인 SQL처럼 INNODB_TRX와 data_locks에서 해당 항목이 실제로 사라졌는지 다시 확인해야 합니다.
5. 방치하면 언제 정리되는가
Sleep 연결은 wait_timeout이 지나면 서버가 끊고, 이때 열린 트랜잭션도 롤백됩니다. 문제는 이 값의 기본값이 28800초(8시간)라는 점입니다. 커밋을 빠뜨린 연결이 그 시간 동안 행 잠금을 유지할 수 있다는 뜻입니다. 커넥션 풀처럼 연결을 계속 재사용하는 환경에서는 wait_timeout에 도달하지 않는 경우도 많습니다. 서버 설정에 기대기보다 애플리케이션의 종료 경로를 고치는 것이 근본 대책입니다.
대화형 클라이언트로 접속한 연결에는 wait_timeout 대신 interactive_timeout(기본값 역시 28800초)이 세션 값으로 적용됩니다. GUI 도구로 열어 둔 연결의 실제 제한 시간을 확인하려면 해당 세션의 @@session.wait_timeout을 조회하는 것이 정확합니다.
6. 반복 발생 시 애플리케이션에서 점검할 곳
- 예외 처리 경로에서 ROLLBACK이 빠지지 않았는지
- 트랜잭션을 연 채 외부 API 응답이나 사용자 입력을 기다리지 않는지
- 커넥션 풀에 연결을 반환할 때 미완료 트랜잭션을 롤백하도록 설정되어 있는지
- Workbench, DBeaver 같은 관리 도구에서 수동 커밋 모드로 작업한 뒤 열어 둔 연결이 없는지
마지막 항목은 제가 운영에서 가장 자주 만난 경우입니다. 사용자가 DBeaver로 운영 DB에 접속해 짧은 쿼리를 실행한 뒤 COMMIT을 하지 않고 창을 열어 두면, 세션은 Sleep 상태로 남고 트랜잭션은 계속 열려 있습니다. client_host가 애플리케이션 서버가 아니라 개인 PC 주소라면 이 경우를 먼저 의심해 볼 만합니다.
7. 잠금 대기가 없어도 위험한 이유: History list length
커밋을 잊은 세션은 다른 세션을 막지 않더라도 문제를 만듭니다. 제가 겪은 사례에서는 DBeaver 세션이 COMMIT 없이 Sleep 상태로 남아 있는 동안 SHOW ENGINE INNODB STATUS의 History list length가 약 100만(1M)까지 늘어났고, 해당 세션을 KILL로 정리한 일이 여러 번 있었습니다.
History list length는 아직 정리(purge)되지 못한 undo 기록의 양입니다. 오래 열려 있는 트랜잭션이 과거 시점의 데이터를 볼 수 있는 상태로 남아 있으면, InnoDB는 그 이후에 생긴 변경 이력을 지우지 못하고 계속 쌓아 둡니다. 이 값이 커지면 undo 공간이 늘어나고, 일반 조회가 긴 버전 이력을 거슬러 올라가야 해서 느려질 수 있습니다.
그래서 이번 실험처럼 잠금 행이 1건뿐이고 대기 중인 세션이 없더라도, 트랜잭션 경과 시간이 길다면 History list length를 함께 확인하는 것이 좋습니다. 아래 두 방법 중 하나로 볼 수 있으며, 이번 실험에서는 측정하지 않았습니다.
-- TRANSACTIONS 섹션의 History list length 확인
SHOW ENGINE INNODB STATUS;
-- 같은 값을 숫자로 조회
SELECT NAME, COUNT
FROM information_schema.INNODB_METRICS
WHERE NAME = 'trx_rseg_history_len';
진단 순서 정리
이번 실험에서 연결 69는 Sleep 상태였고 현재 SQL은 NULL이었습니다. 그런데도 20초 동안 열린 트랜잭션과 두 개의 잠금 항목이 유지되었습니다. 세션 A에서 ROLLBACK하자 연결은 그대로 남았고 트랜잭션과 잠금만 사라졌습니다.
커밋 누락을 추적할 때는 다음 세 가지를 따로 확인해야 합니다.
- SQL 실행 상태 — threads의 PROCESSLIST_COMMAND, INNODB_TRX의 TRX_QUERY
- 트랜잭션 생존 여부 — INNODB_TRX 존재 여부와 TRX_STARTED 기준 경과 시간, 오래 열려 있다면 History list length
- 잠금 보유와 대기 관계 — data_locks, data_lock_waits, 그리고 DDL 대기라면 metadata_locks
Sleep 상태나 autocommit 값, 다른 세션에서 보이는 데이터는 이 판단의 근거가 되지 못합니다. INNODB_TRX로 후보를 찾고, 연결 정보와 잠금을 묶어 확인한 뒤, 조치 전후 숫자를 비교해 두면 원인과 해결을 모두 설명할 수 있는 진단 기록이 남습니다.




댓글 0
첫 댓글을 남겨보세요.