Redis 락을 DEL로 풀었다가 남의 락을 지운 이유
문제 발생
배치가 중복 실행되지 않도록 Redis 락을 썼습니다.
const ok = await redis.set("lock:report", "1", "NX", "PX", 30000);
if (!ok) return;
await generateReport(); // 가끔 30초를 넘긴다
await redis.del("lock:report"); // 여기서 사고리포트 생성이 30초를 넘긴 날, 두 프로세스가 동시에 돌았습니다.
원인 분석
락이 만료된 뒤의 DEL은 남의 락을 지웁니다.
- A가 락을 잡는다(만료 30초)
- A의 작업이 35초 걸린다 → 30초에 락이 자동 만료
- B가 락을 새로 잡는다
- A가 작업을 끝내고
DEL lock:report→ B의 락이 사라진다 - C가 락을 잡는다 → B와 C가 동시에 실행
Redis 문서가 이 문제를 정확히 설명하며 해법을 제시합니다 — 고정 문자열 대신 추측할 수 없는 큰 랜덤 문자열(토큰)을 설정하고, DEL로 해제하는 대신 값이 일치할 때만 키를 지우는 스크립트를 보내라는 것입니다. 그 이유도 적혀 있습니다 — 만료 시간이 지난 뒤 클라이언트가 락을 해제하려다 나중에 락을 획득한 다른 클라이언트가 만든 키를 지우는 일을 막기 위해서입니다.
문서가 제시하는 해제 스크립트가 이것입니다.
if redis.call("get",KEYS[1]) == ARGV[1]
then
return redis.call("del",KEYS[1])
else
return 0
end해결 방안
- 토큰을 값으로 넣습니다.
const token = crypto.randomUUID();
const ok = await redis.set("lock:report", token, "NX", "PX", 30000);-
해제는 Lua로 원자적으로 합니다. 문서 안내대로
EVAL <script> 1 lock:report <token>형태로 호출합니다. "GET 해서 같으면 DEL"을 두 번의 명령으로 하면 그 사이에 똑같은 경합이 생깁니다. -
만료 시간을 작업 시간보다 넉넉히 잡거나 갱신합니다. 작업이 길어질 수 있으면 진행 중에 주기적으로 TTL을 연장하고, 연장에 실패하면 작업을 중단합니다.
-
락이 없어도 안전하도록 만듭니다. 이 방식은 단일 인스턴스 기준이고, 문서 자신도 더 강한 보장이 필요하면 Redlock을 검토하라고 안내합니다. 그보다 나은 접근은 작업 자체를 멱등하게 만드는 것입니다 — DB unique 제약으로 중복 결과를 거부하면 락이 실패해도 데이터는 깨지지 않습니다.
-
SET이 기존 TTL을 지운다는 점도 기억합니다. 문서 그대로 성공한SET에서는 이전에 설정된 TTL이 폐기됩니다. 값을 갱신하면서 만료를 유지하려면KEEPTTL이 필요합니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.