서버나 프로세스가 공유 자원에 동시 접근 시, 하나만 접근하도록 보장하는 동기화 메커니즘
Lock 정보 저장 위치, Lock 획득/해제 프로토콜에 따라 구현 방법이 달라짐
권한 소유자 정보는 외부 저장소를 활용
특정 키 존재 여부를 통해 분산락 구현
NX (set if Not eXist) : 독점권 확보, 상호 배제 (Mutal Exclusion) 보장
"키가 존재하지 않을 때에만 저장"하는 옵션
가장 먼저 도달한 요청 하나만 성공 보장
EX/PX : 자동 해제, Deadlock 방지
Lock 획득 서버가 장애 발생 시 무한 대기를 방지하기 위한 안전 장치
EX : Seconds 단위로 만료 시간 설정
PX : Milliseconds 단위로 만료 시간 설정
Lua 스크립트
Redis 서버에서 Lua 언어로 작성된 코드 수행
여러 Redis 명령을 원자적으로 묶어 데이터 정합성 보장
Pub/Sub 기반 로직
Redis의 Publish/Subscribe 메커니즘을 활용한 실시간 메시징 패턴
Publisher가 특정 채널에 메시지 발행 시 채널 구독한 모든 Subscriber가 즉시 메시지 수신
분산 환경에서 Lock 안정성 높이는 로직
Redission
NX, Pub/Sub 기반 대기 로직을 라이브버리 수준에서 감춰 주는 클라이언트
NX와PX논리를 포함한 루아 스크립트를 Redis 서버로 보냄
Watchdog
EX/PX 설정 시 Lock 풀려버리는 문제 발생 방지하여, 작업 진행 중일 때 만료시간 연장lock.tryLock()
내부NX,PX로직과Watchdog의 연장 로직 포함 함수
예시 코드
@Service
@RequiredArgsConstructor
public class StockService {
private final RedissonClient redissonClient;
private final StockRepository stockRepository;
public void decreaseStock(Long productId, Long quantity) {
// 1. 락의 고유 키 생성 (상품별로 락을 걸기 위함)
RLock lock = redissonClient.getLock("lock:stock:" + productId);
try {
// 2. 락 획득 시도 (최대 5초 동안 대기, 획득 후 3초간 유지)
boolean available = lock.tryLock(5, 3, TimeUnit.SECONDS);
if (!available) {
System.out.println("락 획득 실패");
return;
}
// 3. 비즈니스 로직 수행 (임계 영역)
Stock stock = stockRepository.findById(productId).orElseThrow();
stock.decrease(quantity);
stockRepository.save(stock);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 4. 락 해제 (현재 스레드가 락을 가지고 있는 경우에만 해제)
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
임시 순차 노드 (Ephemeral Sequential Node)와 Watch 기능을 활용해 줄을 세움
모든 클라이언트는 공통 경로에 자식 ephemeral+sequential 자식 노드를 생성함
모든 자식 노드를 조회해 가장 번호가 작은 노드가 Lock을 가진 것으로 간주
나보다 바로 앞 번호 노드에 watcher를 걸고 노드 삭제 때 까지 대기
Curator Framework
넷플릭스에서 만든 라이브러리
RetryPolicy
세션/네트워크 이슈 시 재시도 전략 정의
대부분RetryNTimes또는ExponentialBackoffRetry사용InterProcessMutex
ephemeral sequential 노드를 만듦
자식 노드 목록/앞 노드 watch로 Lock 구현
예시 코드
@Configuration
public class ZookeeperConfig {
@Bean(destroyMethod = "close")
public CuratorFramework curatorFramework() {
RetryPolicy retryPolicy = new RetryNTimes(
3, // 최대 재시도 횟수
1000 // 재시도 간격(ms)
);
CuratorFramework client = CuratorFrameworkFactory.newClient(
"localhost:2181", // ZK 서버 주소
retryPolicy
);
client.start();
return client;
}
}
@Service
@RequiredArgsConstructor
public class ZooKeeperStockService {
private final CuratorFramework client;
public void decreaseStock(String productId) {
// 1. 락 경로 설정 (ZooKeeper의 디렉토리 구조 사용)
String lockPath = "/locks/products/" + productId;
InterProcessMutex lock = new InterProcessMutex(client, lockPath);
try {
// 2. 락 획득 시도 (최대 5초 대기)
if (lock.acquire(5, TimeUnit.SECONDS)) {
try {
// 3. 비즈니스 로직 수행
System.out.println("락 획득 성공! 재고 감소 로직 수행 중...");
// stockRepository.decrease(...)
} finally {
// 4. 락 해제
lock.release();
}
} else {
System.out.println("락 획득 실패");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
MySQL/Oracle 역시 Lock 저장소로 사용 가능
GET_LOCK(name, timeout) / RELEASE_LOCK(name) 함수로 이름 기반 사용자 Lock 사용lock_name PK를 두고 비관적 락으로 점유하게 만들어 사용 가능| 항목 | Redis(Redisson) | ZooKeeper(Curator) | DB |
|---|---|---|---|
| 성능 | 최고 (메모리 기반) Watchdog 자동 TTL 연장 | 높음 (지속적 노드 관리) | 낮음 (디스크 I/O) |
| 안정성 | 높음 ( Pub/Sub+Watchdog,네트워크 파티션 시 Lock 해제 위험) | 최고 (ZAB 프로토콜, CAP-TC 준수) | 중간 (트랜잭션 격리, Deadlock 위험) |
| 재시도 방식 | Exponential Backoff 자동, leaseTime 기반 | acquire(timeout) 명시적 재시도 | SELECT FOR UPDATE |
| 추천 상황 | 고TPS 웹 서비스, 캐싱+락, 짧은 Critical section | 금융/결제, 장기 Lock, 강한 일관성 | 기존 DB 환경, 간단 Lock, 트랜잭션 연계 |