이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
공부 › DB › 확장과 분산
Redis의 한 줄 처리
명령 하나가 느리면 모두가 기다린다
Redis는 명령을 스레드 하나가 한 번에 하나씩 차례로 실행합니다. 그래서 락 없이도 명령 하나하나가 끼어들 틈 없이 실행되지만, 오래 걸리는 명령이 하나 끼면 그 뒤의 모든 명령이 줄을 섭니다.
일부만 실행으로 확인 · Redis 8.10.2에서 원리를 측정해 확인(시간 값은 예시)
이 실습에 나오는 개념
- Redis
- 데이터를 메모리에 저장하는 키-값 저장소다. 문자열뿐 아니라 리스트, 해시, 집합, 정렬된 집합 등의 자료구조를 제공한다. 메모리에서 동작해 매우 빠르며 캐시, 세션 저장소, 분산 락, 순위표, 요청 수 제한 등에 쓰인다. 명령을 하나의 스레드에서 차례로 실행한다.
- 캐시
- 자주 사용하는 데이터나 계산 결과를 빠르게 접근할 수 있는 곳에 미리 저장해 두고 재사용하는 것이다. 원본(주로 데이터베이스)을 매번 조회하지 않아 응답이 빨라지고 원본의 부하가 줄어든다. 대신 원본이 바뀌었을 때 캐시의 값이 오래된 상태로 남는 문제를 관리해야 한다.
- 원자성(Atomicity)과 CAS
- 원자성은 여러 단계로 이뤄진 연산이 중간에 끼어들 틈 없이 하나처럼 실행되는 성질이다. CAS(Compare-And-Swap)는 "현재 값이 내가 읽었던 값과 같을 때만 새 값으로 바꾼다"를 하나의 원자적 연산으로 수행하는 방법으로, 락 없이 안전한 갱신을 가능하게 한다. AtomicInteger 등이 이를 이용한다.
- 이벤트 루프
- 소수의 스레드가 여러 연결에서 발생하는 입출력 이벤트를 감지해 차례로 처리하는 구조다. 연결마다 스레드를 두지 않으므로 하나의 스레드가 많은 연결을 담당한다. 이 스레드에서 오래 걸리는 작업이나 블로킹 호출을 하면 그 스레드가 맡은 모든 연결의 처리가 멈춘다.
이렇게 바꿔 보세요
- 처음 설정으로 — KEYS 한 번에 다른 클라이언트들이 0.5초씩 멈춥니다.
- 빠른 명령만 있을 때 — 모든 명령이 1ms 안에 끝납니다.
- SCAN으로 바꾸면 — 조금씩 나눠 실행해서 사이사이에 다른 명령이 끼어들 수 있습니다.
- 큰 컬렉션을 통째로 읽으면 — KEYS가 아니어도 값이 크면 같은 일이 생깁니다.
- 동시에 INCR — 한 줄로 실행되므로 증가가 사라지지 않습니다.
처음 설정으로 실행하면
- Redis는 여러 클라이언트의 명령을 받아 한 줄로 세운 뒤, 스레드 하나가 앞에서부터 하나씩 실행합니다. 명령 하나가 끝나야 다음 명령을 시작합니다.
- 서비스 A의 GET product:1: 0.1ms 동안 실행됐습니다.
- 서비스 B의 GET product:2: 0.1ms 동안 실행됐습니다.
- 관리자의 KEYS user:*: 500ms 동안 실행됐습니다. 이 명령이 실행되는 동안 Redis는 다른 어떤 명령도 처리하지 못합니다.
- 서비스 A의 GET product:3: 앞의 명령이 끝나기를 499.2ms 기다린 뒤 0.1ms 동안 실행됐습니다.
- 서비스 B의 SET cart:9 ...: 앞의 명령이 끝나기를 499.3ms 기다린 뒤 0.1ms 동안 실행됐습니다.
- 서비스 A의 GET product:4: 앞의 명령이 끝나기를 498.4ms 기다린 뒤 0.1ms 동안 실행됐습니다.
실행 결과
- 평소 0.1ms에 끝나던 조회가 499.3ms를 기다렸습니다. Redis에 연결된 모든 서비스가 동시에 느려지고, 타임아웃이 걸려 있으면 캐시 조회가 줄줄이 실패합니다. 운영 중인 Redis에서 KEYS, 큰 컬렉션 통째 조회, 큰 키 삭제(DEL) 같은 명령을 피해야 하는 이유입니다.
코드
// 서비스 A, B: 평범한 캐시 조회
String v = redis.opsForValue().get("product:1"); // 0.1ms
// 관리자 스크립트
Set<String> keys = redis.keys("user:*"); // 전체 키를 한 번에 훑는다
이 모형의 전제
명령의 실행 시간(GET 0.1ms, KEYS 500ms, 큰 리스트 조회 300ms, SCAN 한 번 0.5ms)은 크기의 차이를 보이기 위한 예시입니다. Redis가 명령을 한 스레드에서 차례로 실행한다는 점과 KEYS 대신 SCAN을 쓰라는 권고는 Redis 공식 문서를 따랐습니다. Redis 6부터 네트워크 입출력에는 여러 스레드를 쓸 수 있지만 명령 실행은 여전히 한 스레드입니다. 원리는 실제 Redis 8.10.2에서 확인했습니다(키 100만 개, 원소 50만 개짜리 리스트). KEYS가 도는 동안 다른 클라이언트의 GET이 최대 186ms, 큰 리스트 조회 동안 22ms를 기다렸고(평소에는 3ms 이하), SCAN으로 1,000개씩 나눠 훑는 동안에는 1.5ms를 넘지 않았습니다. 두 클라이언트가 같은 키에 20,000번씩 동시에 보낸 INCR의 최종 값은 정확히 40,000이었습니다. 화면의 시간 값은 이 측정값이 아니라 예시이며, 실제 시간은 데이터 크기와 장비에 따라 달라집니다.
앞 실습: 파티셔닝 · 다음 실습: NoSQL과 문서 모델