여러 사용자가 동시에 같은 데이터를 수정하려고 할 때 경쟁 조건이 발생하는 경우 Redis는 어떻게 대처할까?
Redis는 경쟁 조건을 방지하기 위해 두 가지 접근 방식인 Optimistic Lock, Pessimistic Lock 을 사용한다.
시나리오 | 전략 | 이유 |
재고 감소, 좋아요 수 증가 | Optimistic | 충돌 적고 성능 중요 |
결제, 티켓팅, 중복 요청 방지 | Pessimistic | 무조건 1개만 처리해야 함 |
Optimistic Lock (낙관적 락)
문제가 발생하지 않을 것이라는 낙관적인 개념으로
동시 접근을 허용하되, 마지막에 충돌이 발생하면 롤백하는 방식이다.
Redis에서는 `WATCH`, `MULTI`, `EXEC` 명령어를 사용하여 구현한다.
특징
항목 | 설명 |
장점 | 락을 사용하지 않아 빠르고 병렬 처리에 유리 |
단점 | 충돌이 많으면 retry로 인해 성능 저하 |
적합한 경우 | 업데이트 충돌 가능성이 낮은 경우 |
구현 방식
WATCH mykey # mykey를 감시 시작
MULTI # 트랜잭션 시작
SET mykey "new-value" # 여러 명령어를 큐에 담음
EXEC # 트랜잭션 실행 (다른 클라이언트가 mykey를 바꿨다면 실패)
WATCH 이후 다른 클라이언트가 mykey를 변경하면 EXEC 실행 시 트랜잭션은 실패한다. (롤백)
👉 실패 시 다시 시도(retry) 한다
Pessimistic Lock (비관적 락)
문제가 발생할 것이라는 비관적 개념으로
공유자원에 접근하는 동안 다른 사용자의 접근을 막는다.
Redis에서는 `SENTX` 나 `Redlock` 알고리즘으로 구현한다.
특징
항목 | 설명 |
장점 | 경쟁 없이 안전하게 자원을 보호 |
단점 | 락 획득/해제 비용, 데드락 위험 |
적합한 경우 | 공유 자원에 대한 충돌 가능성이 높고 꼭 순서를 보장해야 하는 경우 |
구현 방식 (SENTX)
SETNX lock_key "locked" # 락 획득 시 1 반환 (성공)
EXPIRE lock_key 10 # 타임아웃 설정 (10초)
... # 자원 사용
DEL lock_key # 락 해제
Redlock 알고리즘 (비관적 락)
단일 Redis 서버에서 SENTX로 락을 구현 시 Redis 서버가 다운되면 락 자체도 유실되어 신뢰성이 떨어진다.
∴ 분산 환경에서는 여러 Redis 노드에 락을 동시에 걸고, 일부가 실패해도 안전하게 락을 유지해야 한다.
📌 작동 원리
N개의 서로 다른 Redis 인스턴스 중에서 과반수 이상 (M)에게 락을 성공적으로 설정하면 락 획득 성공으로 간주
`M = N/2+1` (보통 N=5, M=3)
특성 | 설명 |
Safety (안전성) | 오직 하나의 클라이언트만 락을 획득할 수 있음 |
Liveness (활성성) | 일부 Redis 노드가 실패하더라도 락을 획득 가능 |
Fault-tolerance (내결함성) | 다수 노드가 살아있으면 시스템은 정상 동작 |
락 획득 과정
- 현재 시간 기록 (밀리초 기준)
- 모든 Redis 인스턴스에 동일 key, 동일 value로 락 시도
- 예: SET lock_key value NX PX 10000
(NX: 해당 키가 존재하지 않을 때만 설정, PX: TTL 10초)
- 예: SET lock_key value NX PX 10000
- 응답을 받은 Redis 인스턴스가 M 이상인지 check
- 총 소요 시간이 TTL 보다 작을 경우 락 획득 성공
- 실패하면 전체 락 해제 후 재시도 (retry with backoff)
락 해제 과정
락을 해제할 때는 반드시 락을 설정한 사용자만 해제할 수 있어야 한다.
락 설정 시 사용하는 랜덤한 고유 값 (UUID 등)을 저장해 두었다가, 락 해제 시 key의 value값이 동일한 경우만 삭제한다.
-- Lua 스크립트 예시 (atomic하게 수행)
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
결론
Redlock은 단일 Redis 인스턴스 기반 락보다 훨씬 안전한 락 시스템이며, 특히 다음과 같은 경우에 적합하다.
- 분산 시스템 환경 (여러 서버에서 동시에 접근)
- 락이 반드시 정확하게 지켜져야 하는 경우 (예: 주문 중복 방지, 결제 동시 처리)
항목 | 안전하게 락 처리하려면 |
TTL | 무조건 설정 (자동 해제용) |
UUID | 락 소유자 식별을 위해 사용 |
비정상 종료 | TTL로 방지, UUID로 정확히 해제 |
장기 작업 | 필요 시 락 연장 로직 구현 |
❓ 응답을 받은 Redis 인스턴스가 M 미만
락을 시도했는데, 성공한 Redis 인스턴스 수가 과반수보다 작은 상황
분산 시스템에서는 일부 노드가 네트워크 단절되거나 느려질 수 있기 때문에
과반수 이상이 동의하지 않으면 `안전하지 않은 락`일 수 있다고 간주한다.
만일 과반수를 넘지 못하는 락을 신뢰한다면:
- 다른 클라이언트도 락을 잡았다고 착각 가능
- 동시에 같은 자원에 접근하여 Race Condition 발생 가능
❓ 락을 얻은 총 소요시간 ≥ TTL
락이 획득되긴 했지만, 그 시점엔 이미 TTL이 거의 끝났거나 지나서 자원 보호가 되지 않는 상황
👉 안전하지 않은 락이므로 실패로 간주한다.
예시
- TTL: 10,000ms (10초)
- Redis 5개 중 3개에서 락을 성공적으로 설정
- 3개에서 락을 모두 얻는 데 걸린 시간: 11,000ms (> TTL)
락을 설정한 시점은 서로 다르고 각 Redis 인스턴스는 락을 잡은 후부터 TTL만큼만 유지
즉, 락을 설정한 순간부터 각 노드는 10초 후에 자동으로 락을 해제한다.
노드 | 락 설정 시간 | TTL 만료 시간 |
Redis A | 0ms | 10,000ms |
Redis B | 5,000ms | 15,000ms |
Redis C | 11,000ms | 21,000ms |
→ 락을 11초에 얻었지만, Redis A에서는 이미 TTL이 만료됨
→ 다른 클라이언트는 Redis A에서 락이 해제된 것으로 보고 다시 락을 잡을 수 있음
🟥 그러면 두 클라이언트가 동시에 자원에 접근하는 Race Condition이 발생할 수 있음
📌 락을 잡는 데 걸린 시간이 TTL 보다 길면, 락을 실패로 간주하고 즉시 해제 수 재시도해야 한다.
DeadLock
클라이언트가 락을 잡고 작업 중 죽거나 네트워크 단절되는 경우, 락을 해제하지 못하게 되는데
이런 상태에서 락이 계속 유지되면 다른 클라이언트가 접근하지 못한다. 👉 데드락 발생
Redis 분산 락에서 Deadlock 예방/회피 방법
방법 | 설명 |
TTL 설정 | 락 자동 만료로 데드락 방지 |
UUID 사용 | 락 소유자 확인으로 안전한 해제 |
재시도 + 백오프 | 랜덤 시간 대기 후 재시도 → 과도한 경합으로 인한 데드락 가능성 감소 |
락 순서 고정 | 교착 상태 순환 대기 방지 |
락 연장 (신중) | 긴 작업 시 락 유효 기간 연장 |
최소 락 시간 | 락 획득 기간 최소화 |