티스토리 뷰
분산락을 사용할 때 락 해제 실패(lock release failure) 문제는 큰 위험 요소가 될 수 있습니다. 만약 락이 정상적으로 해제되지 않으면 데드락(Deadlock) 이 발생하거나 리소스가 불필요하게 점유되는 문제가 생길 수 있습니다. 아래에서 주요 원인과 해결책을 설명드릴게요.
락 해제 실패의 주요 원인
애플리케이션 비정상 종료
- 락을 획득한 애플리케이션이 예상치 못한 종료 (Crash, OOM, Kill Signal) 가 발생하면 락이 유지됩니다.
- 락을 잡은 프로세스가 락을 해제하지 못하고 종료될 경우, 다른 프로세스가 락을 기다리면서 데드락이 발생할 수 있습니다.
TTL(Time-To-Live) 설정 미비
- Redis에서 SETNX로 락을 설정한 후 TTL(EXPIRE)을 설정하지 않으면, 락이 영구적으로 유지될 가능성이 있습니다.
- 락을 해제하는 로직이 정상적으로 실행되지 않으면, 락이 자동으로 풀리지 않습니다.
락 해제 시 잘못된 키 삭제
- 락을 획득한 클라이언트가 다른 클라이언트의 락을 해제하는 경우입니다.
- 예를 들어, 락 키(lock_key)가 만료되었고 다른 프로세스가 같은 키를 다시 생성한 경우, 기존 프로세스가 락 해제 요청을 보냈을 때 의도치 않게 다른 프로세스의 락을 삭제할 가능성이 있습니다.
네트워크 지연 및 분산 환경 이슈
- 락을 획득한 노드에서 락 해제 요청을 보냈지만, 네트워크 이슈로 인해 요청이 늦게 도착하는 경우, 이미 락이 만료되었거나 다른 노드에서 새로운 락이 설정될 수 있습니다.
- 이 경우, 원래 락을 획득한 프로세스가 늦게 도착한 해제 요청으로 다른 프로세스의 락을 해제해버리는 문제가 발생할 수 있습니다.
해결책
TTL(Time-To-Live) 설정 필수
- 락을 획득할 때 반드시 TTL을 설정하여 일정 시간이 지나면 자동으로 해제되도록 만들어야 합니다.
- Redis에서는 SET key value NX PX timeout 명령을 사용하면 원자적으로 TTL을 설정할 수 있습니다.
SET lock_key "unique_value" NX PX 5000
- NX: 키가 없을 때만 설정
- PX 5000: 5초 후 자동 만료
락 해제 시 확인 절차 추가 (락 소유 검증)
- 락을 해제할 때, 현재 락이 자신이 설정한 락인지 확인한 후 해제해야 합니다.
- Redis에서는 Lua 스크립트를 활용하여 이를 원자적으로 수행할 수 있습니다.
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
- 위 스크립트는 락을 획득한 클라이언트가 설정한 값(ARGV[1])이 현재 락의 값과 같을 때만 락을 삭제합니다.
Java 코드 예제는 다음과 같습니다. (Redisson 없이 구현)
public boolean releaseLock(Jedis jedis, String lockKey, String uniqueValue) {
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
Object result = jedis.eval(luaScript, Collections.singletonList(lockKey),
Collections.singletonList(uniqueValue));
return "1".equals(result.toString());
}
Redlock 알고리즘 적용 (다중 노드 환경에서 신뢰성 보장)
- Redis를 단일 노드에서 사용하면 장애 발생 시 락 정보가 유실될 수 있기 때문에 Redlock 알고리즘을 적용하면 더 안전한 락 처리가 가능합니다.
- Redlock은 여러 개의 Redis 노드(3개 이상) 에 락을 동시에 설정하고, 과반수 이상의 노드에서 성공했을 때만 락을 획득하는 방식입니다.
- Redisson에서는 이미 이를 지원하고 있습니다.
Redisson을 활용한 분산락 구현 예제는 다음과 같습니다.(Spring Boot)
@Autowired
private RedissonClient redissonClient;
public void processWithLock() {
RLock lock = redissonClient.getLock("myLock");
try {
if (lock.tryLock(10, 5, TimeUnit.SECONDS)) { // 10초 동안 시도, 5초 유지
try {
// 락이 획득된 경우 실행할 비즈니스 로직
System.out.println("락 획득 성공");
} finally {
lock.unlock();
}
} else {
System.out.println("락 획득 실패");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
- tryLock() 을 사용하면 락 획득을 시도하는 최대 시간과 락 유지 시간을 설정할 수 있어 비정상 종료 시에도 락이 일정 시간 후 자동 해제됩니다.
락 만료 시간 연장 (Watchdog 방식 적용)
- 락을 오래 유지해야 하는 경우 Watchdog 방식으로 TTL을 자동 연장하는 방법이 있습니다.
- Redisson에서는 lock.lock()을 사용하면 자동으로 TTL을 연장해주는 기능을 제공합니다.
RLock lock = redissonClient.getLock("myLock");
lock.lock(); // 기본적으로 30초 유지하며, 작업이 계속 진행되면 자동 연장됨.
- Redisson의 Watchdog 기능 덕분에 프로세스가 종료되지 않는 한 락이 해제되지 않고 유지됩니다.