연습 기록을 저장하는 요청은 세션을 읽고 값을 갱신한 뒤 응답에 넣을 행을 다시 읽습니다. 그 사이 같은 기록을 삭제하면 마지막 조회가 대상을 잃을 수 있습니다. 저장이 끝날 때까지 삭제가 기다리도록 저장 트랜잭션에서 세션 행을 SELECT … FOR UPDATE로 잠갔습니다.
두 작업을 서로 다른 스레드에서 출발시키는 것만으로는 이 잠금을 검사하기 어렵습니다. 스케줄링에 따라 삭제 요청이 저장보다 늦게 시작하면 두 작업이 실제로 겹치지 않습니다. 일정 시간 멈춘 뒤 삭제가 아직 끝나지 않았다고 단언해도, 삭제 스레드가 DB에 도달했는지 알 수 없습니다.
DB에서 잠금 대기 관찰
테스트에서 트랜잭션을 열고 먼저 저장 경로의 잠금 조회를 실행했습니다. 다음으로 다른 연결에서 같은 세션의 삭제 갱신을 시작했습니다. 첫 트랜잭션을 열어 둔 채 performance_schema.data_lock_waits와 data_locks를 조회해, 세션 테이블의 기본 키 인덱스에서 해당 행을 기다리는 잠금이 생겼는지 확인했습니다.
대기를 관찰한 뒤 첫 트랜잭션을 끝내고, 삭제 갱신도 끝날 때까지 기다렸습니다. 저장 중에는 새 메모를 읽을 수 있었고, 커밋 뒤 삭제가 끝나자 삭제 시각이 채워지고 메모가 비었습니다. 대기가 보이기 전에 삭제가 완료되면 테스트는 실패합니다. 조회에는 마감 시간을 두고, 테스트가 실패하더라도 두 작업을 정리한 다음 픽스처를 지우도록 했습니다.
잠금을 빼고 실패 확인
저장 쿼리에서 FOR UPDATE를 제거한 변형을 돌렸더니 삭제가 잠금을 기다리지 않고 끝났다는 오류로 실패했습니다. 쿼리를 되돌리자 대기를 관찰했고 검사도 통과했습니다. 이때 관찰한 값은 대기가 있었는지 여부입니다. 운영 요청이 평균 몇 밀리초 기다리는지 재는 검사와는 범위가 다릅니다.
이 검사를 실행하는 임시 DB 계정에는 잠금 관찰 테이블 두 곳을 읽을 권한이 필요했습니다. 그 권한은 테스트 계정에만 사용했습니다. 검사가 두 요청의 최종 결과뿐 아니라 의도한 경쟁 구간이 실제로 열렸는지도 확인한 덕분에, 잠금 없이 순서대로 실행해도 우연히 통과하는 경우를 구분할 수 있었습니다.

댓글
댓글과 반응은 GitHub 계정으로 로그인해 남길 수 있어요.