What good is a best-effort exclusive lock, anyway?
최선 노력형 배타 잠금은 대체 어디에 쓸까요?
CockroachDB의 SERIALIZABLE 트랜잭션에서 SELECT ... FOR UPDATE는 정확성을 보장하는 잠금이 아니라 경합을 줄이는 최선 노력형 잠금입니다. 정확성은 트랜잭션 재시도가 보장하고, 잠금은 비싼 작업을 반복하는 상황을 줄이는 성능 최적화로 쓰입니다.
- 주제
AI 요약
일회용 코드를 두 요청이 동시에 사용하지 못하게 하려면 데이터베이스에서 행 잠금을 걸 수 있습니다. PostgreSQL과 MySQL은 SELECT ... FOR UPDATE로 트랜잭션이 끝날 때까지 다른 트랜잭션의 해당 행 접근을 막습니다. 하지만 CockroachDB는 기본 격리 수준이 SERIALIZABLE이며, 이 환경에서 SELECT ... FOR UPDATE와 SELECT ... FOR SHARE는 정확성을 보장하는 잠금으로 보면 안 됩니다. 동시 접근 순서가 요청한 대로 지켜지지 않을 수 있기 때문입니다.
정확성은 SERIALIZABLE이 담당합니다
글쓴이의 동료는 PostgreSQL처럼 READ COMMITTED로 격리 수준을 낮추는 방안을 먼저 생각했습니다. 이 수준에서는 FOR UPDATE가 기대한 배타 잠금으로 동작합니다. 다만 격리 수준을 낮추면 여러 동시성 이상이 생길 수 있고, 코드베이스에서 서로 다른 격리 수준의 트랜잭션이 같은 행을 다루면 애플리케이션 로직을 올바르게 유지하기 어려워집니다.
SERIALIZABLE에서는 동시 트랜잭션의 읽기-쓰기 또는 쓰기-쓰기 충돌을 감지합니다. 충돌하면 많아야 하나의 트랜잭션만 커밋하고, 나머지는 처음부터 다시 실행하도록 합니다. 일부 상황에서는 한 트랜잭션이 끝날 때까지 다른 트랜잭션이 기다린 뒤 이어서 실행합니다. 따라서 실행 결과는 트랜잭션이 하나씩 차례로 완료된 경우와 같아집니다. 필요한 행을 트랜잭션 초기에 읽거나 갱신해 충돌을 감지하게 만들고, 재시도 로직을 갖추면 잠금 없이도 정확성을 지킬 수 있습니다.
best-effort 잠금은 경합을 줄입니다
콘서트 티켓 판매가 시작될 때 1만 건의 요청이 같은 좌석 수 행을 갱신한다고 가정해 보겠습니다. 각 요청이 먼저 행을 읽고 결제 같은 비싼 작업을 수행한 뒤 티켓 수를 늘리면, 충돌한 트랜잭션은 처음부터 다시 실행해야 합니다. 값은 정확하게 유지되지만 결제 작업까지 반복될 수 있어 비용이 커집니다.
이때 SELECT ... FOR UPDATE를 쓰면 가능한 경우 동시 트랜잭션이 갱신 전에 기다립니다. 기다리는 동안 다른 요청이 먼저 처리되므로 충돌과 재시도 횟수를 줄일 수 있습니다. 잠금이 모든 요청에 적용되지 않더라도 일부 요청만 기다리면 성능에 도움이 됩니다. 그래서 best-effort는 잠금이 가끔 실패해도 괜찮다는 뜻이 아니라, 정확성은 별도로 보장하고 잠금은 경합 완화를 위한 최적화로 쓴다는 뜻입니다.
CockroachDB 문서에 따르면 이 잠금은 빠른 비복제 메모리 잠금입니다. 잠금이 걸린 범위에서 lease transfer나 range 분할·병합이 일어나면 잠금이 사라질 수 있습니다. 노드 장애가 없어도 발생할 수 있으므로 잠금만으로 정확성을 보장해서는 안 됩니다. SERIALIZABLE은 강한 보장을 제공하지만 비용이 들 수 있습니다. 상황에 따라 조건부 쓰기, 경합이 집중된 범위와 다른 범위에 쓰기, 또는 이런 최선 노력형 대기로 재시도 부담을 줄일 수 있습니다.
Reddit 반응
- @u/Ok_Dust_8620 — 글 감사합니다. 흥미로운 내용입니다. 이 best-effort 잠금을 빠져나가 불필요하고 비싼 계산까지 하는 트랜잭션이 실제로 얼마나 되는지, 다시 말해 잠금이 얼마나 효과적인지 알면 도움이 되겠습니다. 사용자가 몇 시간 동안 작업한 뒤 마지막에 “죄송하지만 전부 다시 시도해야 합니다”라는 말을 듣는 것보다는, 상호작용이 있는 사용자 경험에서는 비관적 잠금이 더 나을 것 같습니다. 이런 느슨한 잠금을 감당할 수 있는 UI 애플리케이션은 많지 않을 것 같지만, 좋은 글이고 흥미로운 내용을 알게 됐습니다.
- @u/winky9827 — 이런 자잘한 내용을 좋아합니다. 나중에 필요할 때 기억하지 못하곤 하지만, 구현 이유를 알면 이해가 됩니다. 프로그래밍 서브레딧에 올라오는 글이 모두 이렇게 유용하고 핵심을 잘 짚으면 좋겠습니다.
원문: Gaultier 블로그 / 번역·요약: Trawling