분산 락 (Distributed Lock) 과 실제 활용 (Redis, ZooKeeper, DB)

이도형·2025년 12월 20일

분산 락 (Distributed Lock)

서버나 프로세스가 공유 자원에 동시 접근 시, 하나만 접근하도록 보장하는 동기화 메커니즘

특징

  • 한 프로세스 안에 스레드 간 자원 접근을 배타적으로 만드는 Lock을 여러 서버/프로세스에 적용
  • 동시성에 대해서 데이터 일관성정합성을 보장

구현

Lock 정보 저장 위치, Lock 획득/해제 프로토콜에 따라 구현 방법이 달라짐
권한 소유자 정보는 외부 저장소를 활용

공통 구현 흐름

  1. Lock 획득 시도
    (실패 시 재시도/대기/실패 반환 중 선택)
  2. 성공 시 임계 구역 실행
  3. 종료 이 후 자신이 쓴 Lock 안전하게 해제

Redis

특정 키 존재 여부를 통해 분산락 구현

  • 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 기반 대기 로직을 라이브버리 수준에서 감춰 주는 클라이언트
NXPX 논리를 포함한 루아 스크립트를 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();
            }
        }
    }
}

ZooKeeper

임시 순차 노드 (Ephemeral Sequential Node)Watch 기능을 활용해 줄을 세움

  • 임시 노드 (Ephemeral Node) : 클라이언트 연결이 끊어지면 자동 삭제되는 노드
  • 순차 노드 (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();
        }
    }
}

DB 기반

MySQL/Oracle 역시 Lock 저장소로 사용 가능

  • MySQL의 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,
트랜잭션 연계
profile
열심히 살고 싶습니다! 일하고 싶습니다 :)

0개의 댓글