뚜껑뚜
Magnolia
뚜껑뚜
전체 방문자
오늘
어제
  • 분류 전체보기 (37)
    • 웹 개발 (7)
      • Java (5)
      • Spring (0)
      • JPA (0)
      • JavaScript (1)
      • jQuery (1)
      • Mustache (0)
      • MyBatis (0)
    • DB (0)
    • CS (1)
      • HTTP (0)
      • etc (1)
    • 오류 로그 (20)
    • ETC (8)
    • 백엔드 로드맵 (0)
    • 개인 프로젝트 (1)
      • XYAB (1)
    • Diary (0)

블로그 메뉴

  • 홈
  • 태그
  • 방명록
  • 글쓰기
  • 관리

공지사항

인기 글

태그

  • Thymeleaf
  • View
  • 인텔리제이
  • 타임리프
  • HTTP
  • 프로젝트
  • 컨트롤러
  • 서블릿
  • get
  • jquery
  • post
  • URI
  • 어댑터
  • MVC
  • 자바스크립트
  • 스레드
  • 로그인
  • Servlet
  • JPA
  • 헤더

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
뚜껑뚜

Magnolia

오류 로그

Redis 대기열 구현 시 Race Condition 발생

2026. 5. 18. 16:26

에러 발생 상황


티켓 예매 오픈 시 사용자가 집중되는 상황을 고려하여 Redis를 사용해 대기열 기능을 구현하였다. 동시 입장 가능 인원을 50명으로 제한한 뒤, K6를 사용해 동시 접속자 200명 시나리오로 부하 테스트를 진행하였다. 그러나 테스트 결과 기댓값인 50명을 초과한 68명이 즉시 입장 처리되는 문제가 발견되었다.

K6 부하 테스트 결과

 █ TOTAL RESULTS
    CUSTOM
    immediate_users................: 68    47.033438/s
    waiting_users..................: 129   89.225199/s

    HTTP
    http_req_failed................: 0.00% 0 out of 197
    http_reqs......................: 197   136.258638/s

 

 

문제 파악


문제의 원인은 입장된 인원을 담는 active의 사이즈 조회와 추가 사이의 연산이 원자적(atomic)이지 않았다는 점이다.

Long activeCount = redisTemplate.opsForSet().size(activeKey);  // 1. 읽기
// ← 여기서 다른 스레드 개입 가능
redisTemplate.opsForSet().add(activeKey, token);               // 2. 쓰기

Spring 애플리케이션은 멀티 스레드 환경에서 동작한다. 즉, 200명이 동시에 요청을 보내면 여러 스레드가 동시에 입장 로직을 처리하는 메서드를 실행한다. 이때 스레드 A가 size()를 호출해 49를 읽은 직후, 스레드 B도 동일하게 size()를 호출해 49를 읽는다. 두 스레드 모두 49 < 50 조건을 만족한다고 판단하고 add()를 실행하면 active에 51명이 쌓인다. 이 과정이 수십 개의 스레드에서 동시에 발생하기 때문에 50명을 초과한 68명이 즉시 입장 처리된 것이다.

 

문제 해결


해결 방법으로 Redis Lua 스크립트를 도입하였다. Redis는 단일 스레드로 동작하며, Lua 스크립트는 Redis 내에서 원자적으로 실행된다. 즉, 스크립트 실행 도중 다른 명령이 개입할 수 없다는 것이 보장된다. 이를 활용하여 active 사이즈 조회와 추가 연산을 하나의 Lua 스크립트로 묶어 원자성을 확보하였다.

 

Lua

local activeCount = redis.call('SCARD', active_key)
if activeCount < max then
    redis.call('SADD', active_key, token)
    return 1  -- 즉시 입장
else
    redis.call('ZADD', queue_key, score, token)
    return 0  -- 대기열 진입
end

Java 레벨의 synchronized나 분산 락(Redisson) 방식도 고려하였으나, synchronized는 다중 서버 환경에서 효력이 없고, 분산 락은 락 획득/해제 과정에서 네트워크 왕복이 추가로 발생한다는 단점이 있다. 반면 Lua 스크립트는 Redis에 명령을 한 번만 전송하면 되므로 성능상 이점이 있고, 다중 서버 환경에서도 Redis를 단일 진입점으로 사용하기 때문에 원자성이 자연스럽게 보장된다.

수정 후 동일한 K6 시나리오로 재검증한 결과, 정확히 50명만 즉시 입장 처리되었으며 나머지 인원은 대기열에 진입하는 것을 확인하였다.

K6 부하 테스트 결과

 █ TOTAL RESULTS
    CUSTOM
    immediate_users................: 50    32.674005/s
    waiting_users..................: 163   106.517255/s

    HTTP
    http_req_failed................: 0.00% 0 out of 213
저작자표시 (새창열림)

'오류 로그' 카테고리의 다른 글

인텔리제이 maven package ~ does not exist error  (0) 2024.05.31
mariaDB JDBC url 파라미터 구분자 오류  (0) 2023.06.16
META-INF\MANIFEST.MF (지정된 경로를 찾을 수 없습니다)  (0) 2023.03.13
org.springframework.web.servlet.PageNotFound - No mapping for GET  (0) 2023.03.02
스프링 관련된 것들이 모두 에러날 때  (0) 2022.05.19
    '오류 로그' 카테고리의 다른 글
    • 인텔리제이 maven package ~ does not exist error
    • mariaDB JDBC url 파라미터 구분자 오류
    • META-INF\MANIFEST.MF (지정된 경로를 찾을 수 없습니다)
    • org.springframework.web.servlet.PageNotFound - No mapping for GET
    뚜껑뚜
    뚜껑뚜

    티스토리툴바