여기어때 유저혜택개발팀에서 신규 유저 리워드 이벤트 시스템 개발 중 MySQL InnoDB 데드락을 추적하고, 결국 락 자체를 줄이는 방향으로 해결한 과정을 다룬다. Kafka 기반 비동기 이벤트 처리와 동시성 제어가 얽힌 실전 사례다.
핵심 포인트- INSERT IGNORE는 중복 키 확인 시 S-Lock을 잡고, 이어서 FOR UPDATE가 X-Lock 승격을 요청하면서 두 트랜잭션이 순환 대기에 빠졌다
- ON DUPLICATE KEY UPDATE는 X-Lock을 직접 잡아 승격 문제가 없었으나 매번 불필요한 쓰기가 발생했다
- 두 번째 데드락은 이벤트 경로마다 락 획득 순서가 달라 ABBA 순환 대기가 발생한 패턴이었다
- 근본 원인은 rookie_reward 테이블의 카운터 필드 — 모든 이벤트가 같은 Hot Row를 수정해야 했다
- 카운터를 rookie_reward_step 집계로 대체하자 FOR UPDATE 자체가 불필요해졌다
- REQUIRES_NEW 별도 트랜잭션으로 생성 시 잡히는 S-Lock의 수명을 메시지 전체가 아닌 생성 단위로 제한했다
상세 정리- 시스템 구조: 신규 유저 구매→이용완료→포인트 지급 이벤트가 Kafka At-Least-Once로 처리되며, 순서 보장 불가 + 중복 가능 전제로 설계 필요
- 1차 접근: rookie_reward(유저 단위 행)를 SELECT FOR UPDATE로 잠가 순차 처리 시도, 단건은 정상 작동
- INSERT IGNORE 전환 시도: ON DUPLICATE KEY UPDATE의 불필요한 UPDATE를 없애려 변경했더니 데드락 발생
- 1번째 데드락 원인: INSERT IGNORE가 중복 확인 시 S-Lock 보유, 같은 트랜잭션 내 FOR UPDATE가 X-Lock 승격 요청 → 두 트랜잭션이 S-Lock 보유한 채 X-Lock 대기 → 순환 대기
- InnoDB 락 특성: S-Lock끼리는 호환되므로 두 트랜잭션 모두 같은 row에 S-Lock 보유 가능, 그 뒤 X-Lock 승격 요청에서 교착
- 2번째 데드락 원인: 레저티켓 체크아웃 경로가 rookie_reward_step → rookie_reward 순서로 락 획득, 나머지는 반대 순서 → ABBA 패턴
- ABBA 해결: 팀 전체 "rookie_reward → rookie_reward_step" 순서로 통일, EntityManager.refresh로 JPA 1차 캐시 최신성 확보
- 근본 재검토: orderCounter/checkoutCounter/rewardCounter가 모든 이벤트를 Hot Row로 수렴시킨 설계 결함 발견
- 카운터 제거: step 테이블 집계 쿼리로 대체, 대부분 이벤트에서 rookie_reward UPDATE 불필요 → FOR UPDATE 불필요
- REQUIRES_NEW 트랜잭션: 생성 S-Lock의 수명을 메시지 전체 처리 시간이 아닌 생성 단위로 제한 + REPEATABLE READ 스냅샷 문제 해결
- 대안 검토: Redis 분산 락(락↔트랜잭션 경계 불일치 문제), 낙관적 락(충돌 빈도 높아 재시도 비용 큼) 모두 기각
- 최종 결론: "어떻게 락을 잘 걸까"가 아니라 "이 락이 정말 필요한가"를 먼저 물어야 한다
왜 읽나MySQL InnoDB 데드락을 디버깅하고 락 자체를 없애 해결하는 과정을 따라가는 백엔드·데이터 엔지니어에게 실전 레퍼런스.