Redis SETNX 분산 락 — TTL·소유권·원자적 해제
같은 스케줄 작업을 여러 Pod 가 동시에 실행하면 안 되는 상황이 있습니다.
Redis SETNX 로 분산 락을 걸어 한 Pod 만 실행하게 하면 되는데, 원리가
간단한 것에 비해 잘못 짜기 쉬운 데가 세 군데 있습니다.
SETNX — 키가 없을 때만 SET
SETNX 는 “SET if Not eXists” 입니다. 키가 없을 때만 값을 넣습니다. 이걸로
“내가 먼저 락을 잡았다” 를 표현합니다. 이미 있으면 다른 Pod 가 실행 중이라는
뜻이니 건너뜁니다.
acquired = redis_client.set(
lock_key,
lock_value,
nx=True, # 키 없을 때만 SET
ex=ttl_seconds, # TTL도 같이 (원자적)
)
if not acquired:
return # 다른 Pod가 실행 중 → 건너뜀
nx 와 ex 를 한 번의 set 으로 같이 줘야 합니다. SET 따로 EXPIRE 따로
하면 그 사이에 Pod 가 죽어서 TTL 이 안 걸릴 수 있습니다.
TTL이 없으면 데드락
락을 잡은 Pod 가 작업 도중 크래시하면, 락 해제 코드가 안 돌아서 락이 영구히 남습니다. 그러면 그 뒤로 아무 Pod 도 그 작업을 못 합니다. 영원한 데드락입니다.
TTL 을 걸면 크래시해도 그 시간 뒤 락이 자동으로 풀립니다. 작업이 아무리 길어도 안 넘길 시간(예: 5분)으로 잡습니다.
단순한 값이 남의 락을 해제
락 값을 그냥 "1" 같은 걸로 두면 이런 사고가 납니다.
Pod A가 락 획득 → 작업 중 멈춤(느려짐)
TTL 만료 → 락 자동 해제
Pod B가 락 획득 (같은 값 "1")
Pod A가 깨어나서 "내 락 해제" → 실제로는 Pod B의 락을 지움
Pod C가 락 획득 → Pod B와 동시 실행
값이 같아서 A 가 B 의 락을 자기 것으로 착각하고 지웁니다. 그래서 락 값에 UUID 를 씁니다. 해제할 때 “이 값이 내가 넣은 UUID 인가” 를 확인하고, 맞을 때만 지웁니다.
lock_value = str(uuid.uuid4()) # 내 락임을 식별
GET 후 DEL 사이의 경합
“내 락인지 확인하고 지운다” 를 이렇게 짜면 안 됩니다.
GET lock_key # 내 UUID 맞나 확인
DEL lock_key # 맞으면 삭제
GET 과 DEL 은 별개 명령이라, 그 사이에 다른 클라이언트가 값을 바꿀 수 있습니다. 확인은 통과했는데 지우는 순간엔 이미 남의 락일 수 있습니다.
Lua 스크립트로 묶으면 Redis 서버에서 원자적으로 실행돼서, 확인과 삭제 사이에 아무도 못 끼어듭니다.
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
Redis가 죽으면
락을 걸 Redis 자체가 죽으면 어떻게 할지도 정해야 합니다. 락 획득 실패로 모든 스케줄이 멈추게 둘지, 아니면 단독 실행을 허용할지.
Redis 연결 실패 시 예외 처리에서 단독 실행을 허용하는 fallback 을 두는 방법이 있습니다. 잠깐 중복 실행되는 것보다 스케줄 전체가 멈추는 게 더 큰 문제라는 판단입니다. 물론 근본적으로는 Redis 를 HA(Sentinel 이나 Cluster)로 구성해서 Redis 장애 자체를 줄이는 게 우선입니다.
댓글 0