Transactional Outbox message relay 개선하기
리디가 Transactional Outbox 패턴의 message-relay에서 LEFT JOIN 제거, MySQL NOWAIT 도입, SELECT FOR UPDATE 쿼리 최적화로 lock wait 문제 해결
AI 요약
Context
Transactional Outbox 패턴을 사용하는 message-relay에서 processed_message 테이블의 비동기 삭제로 인해 LEFT JOIN 기반 SELECT 쿼리의 성능 저하가 발생했다. 또한 두 개의 message-relay 노드 간 동시성 제어를 위해 분리된 lock 테이블과 Redis pessimistic lock을 사용하면서 lock wait에 의한 DB 부하가 발생했다.
Technical Solution
- processed_message 테이블 제거 및 message 테이블 단일화: message 테이블에 status column을 추가하는 대신 처리 완료 후 동기적으로 메시지를 삭제하는 방식으로 변경하여 LEFT JOIN nested loop anti join 제거
- MySQL 8.0 NOWAIT 도입: SELECT FOR UPDATE 쿼리에 NOAWAIT 옵션을 추가하여 다른 transaction에서 lock을 소유 중일 때 즉시 ER_LOCK_NOWAIT 에러 반환
- lock 테이블 제거: NOWAIT를 통해 lock wait이 발생하지 않으므로 별도의 lock 테이블 제거 및 message 테이블 직접 lock 제어로 변경
- Redis pessimistic lock 제거: NOWAIT로 lock wait 자체가 발생하지 않으므로 MySQL transaction을 감싸던 Redis pessimistic lock 제거
- SELECT FOR UPDATE 쿼리 최적화: message 테이블의 row가 limit 개수보다 적을 때 발생하는 gap lock을 회피하기 위해 먼저 lock 없이 id를 조회한 후 해당 id 목록으로 SELECT FOR UPDATE 실행하는 두 단계 방식 도입
Impact
아티클에 정량적 성능 수치(latency 감소율, throughput 증가량 등)가 명시되지 않았다.
Key Takeaway
Transactional Outbox 패턴 운영 시 불필요한 JOIN 제거와 MySQL의 NOWAIT 같은 non-blocking lock 메커니즘을 활용하면 lock contention을 근본적으로 해결할 수 있으며, 이는 분산 시스템의 lock wait 문제를 Redis 같은 별도 인프라 없이 DB 레벨에서 단순하게 처리하는 사례를 보여준다.
실천 포인트
Transactional Outbox 패턴을 사용하는 메시지 relay 시스템에서 여러 노드 간 동시성 제어가 필요한 경우, 별도의 lock 테이블이나 Redis를 도입하기 전에 MySQL 8.0 이상의 NOWAIT 옵션으로 lock wait를 즉시 실패 처리하는 방식을 먼저 검토하면, lock 대기 시간 제거와 인프라 복잡도 감소를 동시에 달성할 수 있다.