포스트

[TIL] Redis 실전 마스터 클래스 특강 2 - 분산 락과 비동기 처리

Redis 분산 락의 SETNX와 SET NX EX 동작, TTL과 UUID 검증, Redisson RLock과 Pub/Sub 기반 대기 방식, 비동기 처리 선택 이유를 정리한 글입니다.

For the English version of this post, see here.
[TIL] Redis 실전 마스터 클래스 특강 2 - 분산 락과 비동기 처리

배운 내용

MSA 분산 환경의 동시성 제어와 비동기 통신

분산 환경의 최대 적, 동시성 문제와 분산 락

Q 밤 12시의 결제 전쟁: 100명이 동시에 버튼을 누른다면? - 1명의 재고, 여러 명의 결제자

  • 초과 판매 될 것

  • 재고는 1개뿐이나, 결제가 동시에 승인되어 여러 명에게 물건이 팔리는 대참사 발생

→ 초과 판매 (Over-sell) 발생

MSA 환경에서는 여러 대의 서버가 동시에 DB의 재고 데이터를 읽고 수정하려고 달려들기 때문에, 데이터의 정합성이 처참하게 깨짐

  • 이를 막기 위해 분산 락 (Distributed Lock)을 사용해야함
  1. 분산 락 메커니즘 심화: SETNX의 동작 원리

    • 분산 락 = 공용 화장실 열쇠 image

    • 여러 대의 서버(사람)가 화장실(데이터)을 쓰려면, 반드시 화장실 앞(Redis)에 걸려 있는 열쇠(Lock)를 먼저 가져간 사람만 들어갈 수 있게 만듦

      • 초창기 개발자들은 이 열쇠를 만들기 위해 Redis의 SETNX (SET if Not eXists) 명령어를 사용함
        • 키가 존재하지 않을 때만 값을 세팅해라! 라는 뜻

        • 성공하면 1(락 획득), 실패하면 0(락 획득 실패)을 반환

    Q 왜 SET key value NX EX 형태로 한 번에 써야 할까? (재앙 시나리오) image

    • 과거에는 락을 걸고, 영원히 화장실 문이 잠기는 것을 막기 위해 만료 시간(TTL)을 따로 걸었음
      • 12:00:00 - 서버 A가 SETNX lock:sneakers 1 성공 (락 획득!)
      1. 12:00:01 - 서버 A가 EXPIRE lock:sneakers 5를 호출하려고 하는 찰나… 서버 A의 전원이 퍽! 하고 나가버립니다.

      2. 12:00:02 - 만료 시간을 걸지 못한 채 서버가 죽었으니, lock:sneakers 키는 영원히 Redis에 남게 됩니다.

      3. 결과: 다른 모든 서버들은 이 락이 풀리기만을 영원히 기다리는 데드락(Deadlock) 상태에 빠지고, 스니커즈는 아무도 삼 수 없게 됩니다.

    → 이러한 참사를 막기 위해 Redis 2.6.12 버전부터는 락 획득과 만료 시간 설정을 완벽하게 하나의 원자적(Atomic) 명령어로 합침

    1
    
     SET lock:sneakers "server-A-uuid" NX EX 5
    
    • 이렇게 한 줄로 쓰면, 락을 거는 동시에 5초의 만료 시간이 원자적으로 설정돼 중간에 서버가 죽어도 5초 뒤엔 락이 안전하게 해제됨

    • Lock의 만료 시간(TTL) 딜레마: 1초 vs 60초

      • TTL을 너무 짧게 잡았을 때
        • 서버 A가 락을 잡고 결제 로직을 수행하는데 네트워크 지연으로 3초가 걸림

        • 하지만 락은 1초 만에 이미 풀려버림

        • 이때 서버 B가 락을 잡고 들어와 재고를 차감함

        → 상호 배제(Mutual Exclusion) 붕괴 장애가 발생

      • TTL을 너무 길게 잡았을 때:
        • 서버 A가 락을 잡자마자 죽어버림

        • 다른 서버들은 60초 동안 아무 작업도 하지 못하고 계속 실패

        • 사용자들은 1분 내내 결제 에러 창을 보게 됨

      Q 만약 TTL이 3초인데 결제 로직이 5초가 걸려서 중간에 락이 풀려버렸다면, 운 좋게 다른 서버가 안 들어왔더라도, 결제 로직이 끝나고 DEL lock:sneakers로 락을 지워버리면 어떤 황당한 일이 벌어질까 - 본인 락이 아니라 다른 서버가 새로 잡은 락을 지워버림

      1
      2
      3
      4
      5
      
          → 3초 만에 내 락은 이미 자연 소멸했고, 4초 차에 서버 B가 새로 락을 잡았는데, 5초 차에 내가 끝나면서 서버 B의 락을 지워버림
      
          ![image](/assets/img/notion/TIL-Redis-실전-마스터-클래스-특강-2/03-9008952e17.png)
      
          ⇒ 그래서 **락의 value에 UUID 같은 고유값을 넣고, 지울 때 ‘내 UUID랑 맞을 때만 지워라!’라는 검증 로직이 반드시 필요함**
      
  2. 동시성 제어 기술 비교

비교 항목DB 비관적 락DB 낙관적 락Redis 기본 분산 락Redisson (RLock)
원리SELECT FOR UPDATEVersion 컬럼 확인싱글 스레드 SpinlockPub/Sub 기반 이벤트 대기
성능 (처리량)매우 낮음높음 (충돌 없을 때)중간~높음매우 높음
데드락 위험높음없음낮음 (TTL 방어)낮음 (Watchdog 내장)
구현 난이도쉬움보통매우 어려움쉬움
대표 시나리오금융 코어회원 정보 수정가벼운 동시성 제어선착순 쿠폰, 타임세일
  1. 한정 수량 선착순 쿠폰 발급 (실제 코드 딥다이브)

    • ❌ [버전 1] 직접 구현한 Redis 분산 락 (위험 요소 가득!)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      
        public void issueCouponDirect(String userId, String couponId) {
            String lockKey = "lock:coupon:" + couponId;
            String uuid = UUID.randomUUID().toString();
                  
            // [위험 1] 락을 얻을 때까지 무한 루프를 도는 Spinlock.
            // Thread.sleep 없이 돌면 Redis에 초당 수만 번 요청을 때려 Redis CPU가 터집니다.
            while (!redisTemplate.opsForValue().setIfAbsent(lockKey, uuid, Duration.ofSeconds(3))) {
                try { Thread.sleep(50); } catch (Exception e) {} 
            }
      
            try {
                // [위험 2] 락의 TTL(3초)보다 비즈니스 로직이 길어지면 락이 풀려버립니다.
                if (couponRepository.countById(couponId) > 0) {
                    couponRepository.issue(userId, couponId); 
                    // [위험 3] 이 로직이 트랜잭션으로 묶여있다면,
                    // 락이 해제된 이후에 DB 커밋이 발생해 동시성이 깨질 수 있습니다!
                }
            } finally {
                // [위험 4] Exception이 터져도 락이 풀리도록 반드시 finally 안에서 해제.
                // [위험 5] 남이 잡은 락을 내가 지워버릴 수 있습니다. Lua 스크립트로 value 검증 필수!
                if (uuid.equals(redisTemplate.opsForValue().get(lockKey))) {
                    redisTemplate.delete(lockKey); 
                }
            }
        }
      
    • ✅ [버전 2] Redisson을 활용한 분산 락 (★ Watchdog 주의사항 포함)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      
        public void issueCouponRedisson(String userId, String couponId) {
            String lockKey = "lock:coupon:" + couponId;
            RLock lock = redissonClient.getLock(lockKey);
      
            try {
                // 💡 [사용법 1] Watchdog이 동작하는 방식 (기본 권장)
                // 매개변수: (최대 대기 시간, 시간 단위)
                // 10초 동안 락 획득을 대기. 락 획득 시 작업이 끝날 때까지
                // Redisson의 Watchdog이 기본 30초마다 만료 시간을 자동 연장해 줍니다!
                boolean isLocked = lock.tryLock(10, TimeUnit.SECONDS); 
                      
                /* 
                // 💡 [사용법 2] Watchdog이 동작하지 않는 방식 (주의 요망!)
                // 매개변수: (최대 대기 시간, 임대 시간(leaseTime), 시간 단위)
                // 10초 대기 후 락을 획득하면, 비즈니스 로직이 끝나든 말든 무조건 3초 뒤에 락이 해제됩니다.
                // 작업이 3초를 넘어가면 상호 배제가 깨질 수 있으므로, 장애 시 강제 해제용으로만 조심히 써야 합니다.
                boolean isLocked = lock.tryLock(10, 3, TimeUnit.SECONDS); 
                */
      
                if (!isLocked) {
                    throw new RuntimeException("대기열 초과. 잠시 후 다시 시도해주세요.");
                }
      
                // 안전한 비즈니스 로직 수행
                if (couponRepository.countById(couponId) > 0) {
                    couponRepository.issue(userId, couponId);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                // 현재 스레드가 락을 쥐고 있는지 자체 검증 후 안전하게 해제
                if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                    lock.unlock();
                }
            }
        }
      

서비스 간의 우체부: Redis Pub/Sub vs Message Queue

Q 고객이 결제를 완료함, 주문 서비스가 ‘결제 완료 알림’을 발송 서비스로 보내야 하는데, 이때 Redis Pub/Sub을 쓰면 안 되는 결정적인 이유?

  • 메시지가 중간에 날아갈 수도 있음

→ 결제 알림은 무조건 100% 도착해야 하는 핵심 데이터

  • Redis Pub/Sub은 보장성이 없는 라디오 방송 같은 것이기 때문에 중요 로직에 단독으로 사용하면 안됨
  • “Fire-and-Forget”

    • Redis Pub/Sub은 라디오 방송과 같음
      • 동작 원리: DJ(Publisher)가 채널에 사연(메시지)을 보냅니다. 이때 주파수를 맞추고 라디오를 켜둔 청취자(Subscriber)만 그 사연을 들을 수 있습니다.

      • 치명적 한계: 만약 수신 서버가 배포를 위해 잠시 재시작 중이어서 1분간 연결이 끊겼다면? 그 1분간 발송된 메시지는 영원히 허공으로 증발합니다(Fire-and-Forget).

    • 이에 반해 Kafka / RabbitMQ는 우체국과 같음 (큐잉 및 보장) image

      • 메시지를 안전하게 보관하고, 수신 확인(Ack)을 받아야만 삭제함
    비교 항목Redis Pub/Sub (방송국)Kafka (우체국)
    메시지 보관 여부❌ 보관 안 함 (즉시 휘발)⭕ 디스크/메모리에 큐잉 보관
    수신 확인 (Ack)❌ 확인 안 함⭕ Consumer가 Ack를 보내야 큐에서 지움
    처리 보장 수준낮음 (유실 가능성 큼)매우 높음 (At least once 등 보장)
    성능 (속도)극강의 스피드 (레이턴시 < 1ms)높음 (디스크 I/O로 Redis보다 무거움)
    운영 복잡도매우 낮음매우 높음
    대표 사용 시나리오실시간 채팅, 주식 호가 갱신결제 완료 이벤트, 절대 유실되면 안 되는 데이터 스트림

한국 대형 IT 기업들은 Redis를 어떻게 쓰고 있을까

  • Redis는 어떻게 한 명의 직원으로 수만 명을 대응하는가
    • Event Loop 덕분에 단 1개의 스레드로 동시에 수만 개의 연결을 처리함

    • 단, 한 번에 명령어는 1개씩만 실행하여 완벽한 원자성(Actomicity)을 보장함

    • 따라서 곧 끝나는 짧은 명령어만 넣으면, 한 명의 직원이라도 지연 없는 연속 서비스가 가능함

  • 싱글 스레드의 원자성 활용: 동시성 제어와 분산 락
    • [사례 1] 우아한형제들 (배민 B마트): 창고 물류 동시성 제어
      • ① 비즈니스 배경: B마트는 물류센터(DC)에서 지역 거점(PPC)으로 상품 재고를 끝없이 이관하는 창고 관리 시스템(WMS)을 운영합니다.

      • ② 발생한 기술적 문제: ‘재고 할당’ 기능에는 락이 있었으나 ‘할당 취소’에는 락이 없었습니다. 관리자 A가 재고를 할당하는 찰나에, 관리자 B가 취소를 누르면서 상태 값이 꽬이고 실제 창고 재고와 DB 재고가 불일치하는 심각한 사고가 발생했습니다.

      • ③ Redis 도입 의사결정: MySQL DB 락으로 해결할 수도 있었지만, 수천 건의 재고 이동이 일어나는 상황에서 DB에 락을 걸면 커넥션 풀 고갈과 데드락으로 전체 물류 시스템이 멈출 수 있어 가벼운 Redis 분산 락을 선택했습니다.

      • ④ 구체적 구현 방식: ‘이관 요청서 번호’ 단위로 SETNX 기반의 분산 락을 걸어 처리했습니다.

      • ⑤ 도입 후 결과: 동시성 이슈 0건 달성.

      • ⑥ 만약 Redis가 없었다면?: 락 충돌로 배달 라이더들이 재고가 없는 물건을 픽업하러 왔다가 돌아가거나, 물류 창고가 마비되었을 것입니다.

    • [사례 2] 토스 (Toss): 외화 예금 입출금 동시성 제어
      • ① 비즈니스 배경: 고객의 외화 예금 계좌 잔액을 관리하는 핵심 금융 서비스입니다.

      • ② 발생한 기술적 문제: ‘직접 환전’, ‘자동 모으기’, 해외 직구 ‘카드 결제’가 동일한 계좌에 0.1초 만에 동시에 들어오는 상황이 발생할 수 있습니다.

      • ③ Redis 도입 의사결정: 금융 데이터 특성상 초과 출금은 치명적. DB 락만으로는 다중 서버 환경의 분산 트랜잭션을 완벽하고 빠르게 처리하기 부족했습니다.

      • ④ 구체적 구현 방식: Redisson을 활용해 ‘계좌 번호’ 단위로 분산 락을 걸어 동시성을 1차적으로 제어하고, 이후 MySQL DB 단에서 락을 통해 거래 가능 여부를 2차로 검증했습니다.

      • ⑤ 도입 후 결과: 동시성 오류로 인한 잔액 불일치 및 마이너스 통장 발생을 완벽히 차단.

  • 데이터 유실 없이 비동기로 전환하기: 올리브영 선착순 쿠폰
    • [사례 3] 올리브영: 선착순 쿠폰 발급 (Pub/Sub의 역발상)
      • ① 비즈니스 배경: 올영세일 기간, 매일 밤 12시에 선착순 1만 명에게 쿠폰을 발급합니다.

      • ② 발생한 기술적 문제: 12시 정각에 수십만 명의 발급 요청이 쌏아져 메인 DB의 커넥션 풀을 100% 점유해버렸고, 올리브영 전체 사이트가 마비되는 장애가 발생했습니다.

      • ③ Redis 도입 의사결정: 무거운 Kafka 대신 익숙한 Redis를 채택해 동기 방식을 비동기 방식으로 전환했습니다.

      • ④ 구체적 구현 방식 (★모순 해결 포인트): 여기서 질문! “아까 Pub/Sub은 유실 위험이 있어서 중요 로직에 쓰면 안 된다고 하지 않았나요?” 맞습니다! 그래서 올리브영은 데이터 유실을 막기 위해 발급 요청 데이터를 Redis의 List에 RPUSH로 안전하게 적재했습니다. 그리고 Pub/Sub은 단지 비동기 Worker들에게 “처리 시작해!”라고 알리는 가벼운 ‘트리거’ 용도로만 사용했습니다. Worker는 알림을 받으면 List에서 LPOP으로 데이터를 꺼내어 Main DB에 기록합니다.

      • ⑤ 도입 후 결과: 메인 DB 부하를 격리하여 전체 사이트 처리 속도가 2.2배 향상.

분산 락의 함정과 대안

Q 사용자가 인기 콘서트 예매를 하려고 눌렀는데 이미 락이 걸려 실패함, 이때 1) 계속 빙글빙글 로딩을 돌며 재시도하게 놓을까, 2) “이미 선택된 좌석입니다” 하고 바로 에러를 띄울까

  • 콘서트 예매니까 바로 에러 창 띄워서 다른 좌석을 고르게 해주는 게 맞음

→ 무조건 재시도(Spin/Retry)하는 게 정답이 아님. 비즈니스 성격에 따라 전략이 완전히 달라져야 함

  1. 분산 락이 정말 항상 답인가 (락 없이 동시성을 제어하는 방법)
    • 락은 획득-작업-해제의 네트워크 왕복이 3번이나 발생

    • 단순한 카운팅이나 재고 차감이라면 락 없이 Redis의 원자적 명령어(INCR, DECR)를 사용하는 것이 훨씬 빠름

    • 조금 더 복잡한 로직이라면 Redis 2.6부터 지원되는 Lua 스크립트를 통째로 서버에 던지면, 싱글 스레드 환경에서 원자적으로 실행되어 락 없이도 완벽한 제어가 가능함

  2. “Redlock 알고리즘의 논쟁” (단일 노드의 한계를 넘어서)

    Q 단일 Redis Master 노드에 락을 걸었는데, 락 정보가 Slave로 동기화되기 직전에 Master가 죽으면 어떻게 될까 → 새로 승격된 Slave에는 락이 없으니 다른 클라이언트가 락을 또 획득하는 사태 발생

    • 이를 막기 위해 Redis 창시자는 Redlock(레드락)이라는 알고리즘을 제안함
      • 5대 이상의 홀수 개 독립 Redis 노드에 락을 요청해 과반수(3대) 이상 획득 시에만 인정하는 방식

      • 🚨 학계의 논쟁: 참고로 이 Redlock은 학계에서 활발한 논쟁의 대상이 되기도 했습니다. 분산 시스템 연구자인 마틴 클레프만(Martin Kleppmann)은 2016년 ‘How to do distributed locking’이라는 글을 통해 Redlock이 시스템 시계 동기화에 강하게 의존하고 있다는 점을 비판했고, 이에 대해 Redis 창시자인 살바토레 산필리포가 즉각 재반박하는 사건이 있었죠.

      • 💡 결론: 절대 유실되면 안 되는 금융 거래나 엄격한 분산 합의가 필요한 영역에서는 Redlock보다 데이터베이스 트랜잭션이나 Zookeeper 같은 전문 합의 알고리즘이 더 안전할 수 있습니다.

  3. ‘락 획득 실패 시 처리 전략’ (비즈니스와 UX의 조화)
    1. 즉시 실패 (Fail-Fast): 콘서트 예매처럼 대기가 없어 시스템 부하가 가장 적은 방식입니다.

    2. 재시도 대기 (Spin/Blocking Wait): 은행 송금처럼 지연되더라도 반드시 순차적으로 처리되어야 할 때 씁니다. (최대 대기 시간 설정 필수!)

    3. 큐잉 (Queuing): 배달 앱의 폭우 시 주문 접수처럼, 일단 요청을 다 받아 Queue에 밀어 넣고 비동기로 순차 처리하는 방식입니다.


질문 & 오류

Q DB 비관적 락, DB 낙관적 락, Redis 분산 락, Redisson RLock은 각각 뭐고 어떤 차이가 있나요?

A. 동시에 여러 요청이 같은 데이터를 수정하려고 할 때 충돌을 막기 위한 방법들이다.

  • DB 비관적 락(Pessimistic Lock) “내가 작업 끝날 때까지 아무도 접근하지 마” 방식이다.SELECT FOR UPDATE로 데이터를 직접 잠그며, 안전하지만 성능이 느리고 데드락 위험이 있다. 금융 거래처럼 정합성이 가장 중요한 상황에 사용한다.

  • DB 낙관적 락(Optimistic Lock) “일단 같이 수정하고 마지막에 충돌 검사하자” 방식이다.version 컬럼으로 충돌 여부를 확인하며, 충돌이 적으면 성능이 좋다. 회원 정보 수정처럼 동시 수정 가능성이 낮은 경우에 사용한다.

  • Redis 기본 분산 락 Redis에 락(Key)을 만들어 열쇠를 가진 요청만 작업하게 하는 방식이다. Redis의 싱글 스레드 특성을 이용해 빠르게 처리하지만, TTL·재시도·장애 처리 등을 직접 구현해야 해서 어렵다.

  • Redisson RLock Redis 분산 락을 더 안전하고 편하게 사용할 수 있도록 만든 라이브러리 기반 락이다. Pub/Sub 방식으로 대기하고 Watchdog으로 TTL을 자동 연장해준다. 선착순 쿠폰, 타임세일처럼 트래픽이 몰리는 상황에서 많이 사용한다.

즉,

  • DB 락은 데이터 정합성 중심

  • Redis/Redisson 락은 분산 서버 환경의 동시 요청 제어 중심

이라고 이해하면 된다.

Q Redis Pub/Sub

A. 메시지를 보내는 쪽과 받는 쪽을 분리한 알림 구조

여기서 Pub/Sub은 각각 다음을 의미한다.

  • Pub(Publish): 메시지를 발행하는 것

  • Sub(Subscribe): 메시지를 구독하고 기다리는 것

쉽게 말하면 단톡방 알림과 비슷하다.

누군가 단톡방에 메시지를 보내면, 그 방에 들어와 있는 사람들이 바로 메시지를 받는 것처럼 Redis에서도 어떤 이벤트가 발생하면 구독 중인 대상에게 메시지를 전달할 수 있다.

Redisson 락에서는 이 구조를 이용해 락이 풀렸는지 계속 확인하지 않고 기다린다.

락을 가진 요청이 작업을 끝내고 락을 해제하면 Redis가 대기 중인 요청에게 “락 풀렸어”라고 알려준다.

즉, Redis Pub/Sub은 계속 물어보는 방식이 아니라, 풀리면 알려주는 방식이다.

Q 왜 MySQL DB 락이 아니라 Redis 분산 락을 선택했나요?

A. MySQL DB 락으로도 동시성 문제를 해결할 수는 있다.

하지만 물류 시스템처럼 동시에 수천 건의 재고 이동 요청이 발생하는 환경에서는 DB에 직접 락을 거는 방식이 부담이 될 수 있다.

  • DB 락을 사용하면 작업이 끝날 때까지 다른 요청들이 기다려야 한다.

  • 기다리는 동안 DB 커넥션도 계속 사용된다.

  • 요청이 많아지면 커넥션 풀이 부족해질 수 있다.

  • 여러 요청이 서로 락을 기다리면 데드락도 발생할 수 있다.

  • 심한 경우 전체 물류 시스템이 느려지거나 멈출 위험도 있다.

그래서 DB 대신 Redis 분산 락을 사용해 먼저 요청 순서를 제어하도록 했다.

Redis는 메모리 기반이라 DB보다 훨씬 빠르고 가볍게 락을 처리할 수 있다.

즉, Redis 분산 락은 동시 요청이 많은 환경에서 DB 부담을 줄이고 안정적으로 요청 순서를 관리하기 위해 사용한 방식이다.

Q 왜 Redis를 사용해서 동기 방식을 비동기로 전환했나요?

A. 올리브영의 선착순 쿠폰 이벤트에서는 밤 12시에 수십만 명의 요청이 동시에 몰렸다. 기존 방식처럼 요청이 들어올 때마다 바로 DB에 저장하는 동기 방식으로 처리하면 모든 요청이 한 번에 메인 DB로 몰리게 된다.

  • 이로 인해 DB 커넥션 풀이 빠르게 가득 찼고

  • 전체 사이트가 느려지거나 마비되는 문제가 발생했다.

그래서 요청을 바로 DB에 저장하지 않고 Redis에 먼저 쌓아두는 방식으로 구조를 변경했다.

  • 사용자 요청은 Redis List에 저장한다. (RPUSH)

  • Pub/Sub은 Worker에게 “처리 시작해!”라고 알리는 용도로만 사용한다.

  • Worker는 Redis List에서 데이터를 하나씩 꺼내 (LPOP) DB에 저장한다.

즉, 요청을 바로 처리하지 않고 Redis에 잠시 줄 세워두고 나중에 처리하는 비동기 방식으로 변경한 것이다.

이를 통해 메인 DB에 한꺼번에 요청이 몰리는 현상을 줄일 수 있었고, 전체 사이트 성능도 개선할 수 있었다.

Q Redlock 알고리즘은 왜 등장했고, 왜 논쟁이 있었는지

A. 기존 Redis 분산 락은 보통 하나의 Redis Master 서버에 락을 저장하는 방식이었다. 그런데 문제가 하나 있었다.

예를 들어:

  • 클라이언트 A가 Master에 락 획득

  • 아직 Slave로 락 정보가 복제되기 전

  • 갑자기 Master 서버가 죽음

  • Slave가 새로운 Master로 승격됨

이 상황에서는 새로운 Master에는 락 정보가 없기 때문에 다른 클라이언트가 같은 락을 또 획득할 수 있다.

즉, 원래는 한 명만 가져가야 하는 락인데 서버 장애 때문에 두 명이 동시에 락을 가진 것처럼 동작할 수 있는 문제가 생기는 것이다.

이를 해결하기 위해 Redis 창시자는 Redlock(레드락) 알고리즘을 제안했다.

Redlock은:

  • 하나의 Redis만 믿지 않고

  • 여러 개의 독립 Redis 서버에 동시에 락을 요청한 뒤

  • 과반수 이상 락 획득 시에만 성공으로 인정하는 방식이다.

예를 들어 Redis 서버가 5개라면:

  • 3개 이상 락 획득 → 성공

  • 2개 이하 → 실패

이렇게 일부 서버가 죽더라도 안정성을 높이려는 목적이었다.

하지만 이후 논쟁이 발생했다.

분산 시스템 연구자인 마틴 클레프만(Martin Kleppmann)은:

  • Redlock이 시스템 시간 동기화에 많이 의존하고

  • 특정 장애 상황에서는 완벽하게 안전하지 않을 수 있다고 비판했다.

즉, “정말 중요한 금융 거래나 강한 분산 합의가 필요한 시스템에서는 위험할 수도 있다” 라고 주장한 것이다.

그래서 현재는 상황에 따라 다르게 사용한다.

  • 선착순 쿠폰, 재고 처리 같은 일반 서비스 → Redlock 사용 가능

  • 금융 거래, 절대 데이터 오류가 나면 안 되는 시스템 → Zookeeper 같은 더 강한 합의 시스템 사용

이라고 이해하면 된다.