"CPU 코어가 8개인데 1개만 쓰면 나머지는 노는 것 아닌가"라는 질문은 절반만 맞다. 멀티코어 활용이 곧 빠른 시스템이라는 가정이 Redis에는 통하지 않는다. 공식 벤치마크상 Redis의 초당 처리량은 수만~수백만 건, 평균 레이턴시는 1ms 이하다.
작업
파이프라인 미사용
파이프라인 사용
SET(랜덤 키)
초당 약 7만 건
—
SET(단순 키)
초당 약 18만 건
초당 약 153만 건
GET(조회)
쓰기보다 높음
초당 약 181만 건
Redis가 느려지는 대부분의 경우는 싱글 쓰레드여서가 아니라 연산이 무겁거나 너무 큰 데이터를 조작할 때다. 즉 싱글 쓰레드는 약점이 아니라, 빠르게 동작하도록 설계된 결과다.
⚠️ 벤치마크 수치는 환경·키 종류·파이프라인 여부에 따라 크게 달라진다. "싱글 쓰레드라서 빠르다"가 아니라 "싱글 쓰레드여도 빠를 수 있게 설계했다"가 정확한 표현이다 — 인과의 방향을 뒤집지 않는 것이 이해의 출발점이다.
▶️ 관련 스크립트
멀티코어를 활용한다는 것이 무조건 빠른 시스템이라는 가정이 Redis에서는 무조건 통하는 건 아닙니다.
랜덤 키로 테스트하면 초당 약 7만 건의 SET, 단순 키로 하면 초당 18만 건의 SET(메모리 쓰기)이 나옵니다. GET(조회)은 더 높습니다.
파이프라인을 사용하면 GET은 초당 181만, SET은 초당 153만 건까지 처리가 가능합니다.
Redis는 싱글 쓰레드라서 빠른 게 아니라, 싱글 쓰레드여도 빠를 수 있게 설계한 것입니다.
Redis가 느려지는 대부분의 경우는 싱글 쓰레드여서가 아니라 연산이 무겁거나 너무 큰 데이터를 조작할 때입니다.
2. 멀티스레드의 비용과 싱글 쓰레드를 택한 이유
멀티스레드는 공짜가 아니다. 쓰레드가 여러 개면 공유 자원을 잡기 위한 락, 쓰레드 간 컨텍스트 스위칭(레지스터·스택·캐시 저장/복원), CPU 캐시 일관성 유지, 자원 경쟁 비용이 발생한다. 요청이 많을수록 이 오버헤드가 누적된다. 쓰레드 수에 비례해 처리량이 선형으로 늘어난다는 가정은 공유 자원이 없을 때만 성립한다.
Redis는 처음부터 공유 자원 경쟁이 없는 구조로 설계됐다 — 락이 없으면 대기도 없다. 제작자 antirez가 FAQ·블로그에서 밝힌, 멀티스레드가 가능했음에도 싱글을 택한 이유는 세 가지다.
이유
내용
① CPU는 병목이 아님
실제 병목은 메모리 대역폭·네트워크 IO. 코어를 더 써도 이 병목은 안 풀림
② 단순함이 최우선
멀티스레드 프로그래밍은 어렵고 같은 기능 개발이 느려짐. 락·데드락이 없다고 가정하면 코드가 근본적으로 단순해짐
③ 얻을 것보다 잃을 것
리스트 한 쪽에서 LPUSH, 다른 쪽에서 RPOP을 동기적으로 맞추려면 복잡한 조율 코드가 필요
그래서 Redis는 멀티스레드 대신 여러 인스턴스로 수평 확장하기를 권장한다.
⚠️ antirez는 "기술적으로 멀티스레드 구현이 불가능했던 것이 아니다"라고 분명히 했다. 핵심은 병렬화로 CPU 코어를 더 써도 메모리 대역폭·네트워크 IO라는 실제 병목은 해결되지 않으므로, 락이 만드는 복잡성·오버헤드만 떠안는다는 판단이다.
📝 적용 예시
락 오버헤드 비중 예시: GET 연산의 지연이 약 100ns 이하 → 멀티스레드 도입 시 뮤텍스 락 비용이 최대 50ns까지 추가 → 락으로 인한 오버헤드가 단일 연산 시간의 약 50%를 차지할 수 있음.
▶️ 관련 스크립트
쓰레드가 여러 개면 공유 자원을 획득하기 위한 락, 쓰레드 간 컨텍스트 스위칭 비용, CPU 캐시 일관성 비용, 그리고 자원 경쟁 비용이 발생합니다.
Redis는 처음부터 공유 자원 경쟁이 없는 구조로 설계했습니다. 즉 락이 없으면 대기도 없습니다.
쓰레드가 증가함에 따라 처리량이 선형적으로 증가한다는 것은 공유 자원이 없을 때만 성립하는 가정입니다.
싱글 쓰레드를 선택한 첫 번째 이유는 'CPU는 병목이 아니었다'입니다. 실제 병목은 메모리 대역폭과 네트워크 IO입니다.
예를 들어 Redis의 GET 연산이 100ns 이하인데 멀티스레드의 뮤텍스 락 비용이 최대 50ns까지, 즉 락 오버헤드가 연산의 50%까지 차지할 수 있습니다.
3. 빠른 이유 ① 인메모리 — 디스크 IO 제거
첫 번째 이유는 인메모리 기반이라 디스크 IO를 아예 없앤 것이다. 전형적인 관계형 DB는 버퍼 풀(일부 성능용 메모리)이 있어도 기본적으로 디스크 IO가 발생하는 구조다. 반면 Redis는 모든 데이터의 조회·수정 접근이 무조건 RAM에서 일어난다.
저장소
접근 레이턴시
RAM 대비
RAM
50~100ns
기준
NVMe SSD(초고속)
20~200μs
최소 수백 배 느림
SATA SSD
~0.5ms
약 9,000배 느림
HDD
최대 10ms
최대 10만 배 느림
SSD가 아무리 빨라도 RAM과의 격차는 압도적이다 — NVMe가 가장 빠른 경우라도 RAM 100ns 대비 2만ns로 수백 배 차이가 난다. 디스크 기반이 아니어서 데이터 유실 리스크는 있지만, Redis는 RDB 스냅샷과 AOF 로그로 디스크에 영속 보관하는 메커니즘을 제공한다.
⚠️ 인메모리는 속도의 대가로 휘발성이라는 trade-off를 안는다. 그래서 RDB(스냅샷)와 AOF(append-only 로그)는 '성능을 위한 메모리'가 아니라 '유실 방지를 위한 영속화 장치'로, 역할이 버퍼 풀과 정반대임을 구분해야 한다.
📝 적용 예시
단위 환산으로 격차 체감하기: 1μs = 1000ns. NVMe SSD의 가장 빠른 지연 20μs → ns로 환산하면 20×1000 = 20,000ns. 메모리 100ns와 비교하면 약 200배 차이. SATA SSD는 RAM 대비 약 9,000배, HDD는 최대 100,000배 느림.
▶️ 관련 스크립트
첫 번째 이유는 인메모리 기반이기 때문입니다. 디스크 IO를 아예 없애 버린 거죠.
반면 Redis는 디스크 IO가 아예 없고, 무조건 RAM으로부터 모든 데이터의 조회와 수정 접근이 발생합니다.
일반적인 메모리 접근 레이턴시는 50~100ns이고, 하드디스크는 최대 10ms까지 발생합니다. NVMe SSD는 20~200μs입니다.
RAM과 고성능 NVMe는 최소 수백 배 이상 레이턴시 차이가 나고, SATA SSD는 RAM에 비해 9,000배 이상, 일반 HDD는 최대 10만 배 느립니다.
4. 빠른 이유 ② IO 멀티플렉싱과 이벤트 루프
두 번째 이유는 IO 멀티플렉싱이다. 수만 개 클라이언트 연결 중 '준비된 소켓만' 골라 블로킹 없이 처리한다. Redis는 클라이언트 소켓(파일 디스크립터)을 커널에 등록하고, 리눅스 커널의 고성능 이벤트 알림 메커니즘인 epoll을 사용한다. 그리고 이벤트 루프를 돌려 커널이 "읽기/쓰기 준비됨"이라고 알려준 소켓만 즉시 처리한다.
읽기 준비: 클라이언트가 명령을 보내 소켓 버퍼에 데이터가 도착 → 바로 read 가능
쓰기 준비: 응답을 보낼 소켓 버퍼 공간이 충분 → 바로 write 가능
핵심은 커널이 수만 개 FD를 대신 감시하므로 Redis는 소켓마다 별도 쓰레드를 두고 감시할 필요 없이, 단일 쓰레드의 이벤트 루프만으로 준비된 FD만 반환받아 처리한다. 이 논블로킹 IO + IO 멀티플렉싱은 1999년 댄 케겔이 제기한 C10K 문제(서버 하나로 1만 동시 커넥션 처리)의 해법이며, Node.js·Nginx·FastAPI도 같은 원리를 쓴다.
⚠️ "커넥션 1개당 쓰레드 1개" 블로킹 방식은 커넥션 수만큼 쓰레드를 할당해 메모리가 폭증한다. 반면 IO 멀티플렉싱은 감시 작업을 커널(epoll)에 위임해, 쓰레드를 늘리는 대신 '준비된 것만 골라 처리'하는 방식으로 단일 쓰레드가 수만 연결을 감당한다 — 이것이 동시성을 병렬성 없이 푸는 방법이다.
▶️ 관련 스크립트
두 번째 이유는 IO 멀티플렉싱입니다. 수많은 클라이언트 연결 중 준비된 소켓만 골라 블로킹 없이 처리하기 때문에 빠릅니다.
Redis는 클라이언트의 소켓을 커널에 등록합니다. epoll은 리눅스 커널이 제공하는 고성능 IO 이벤트 알림 메커니즘입니다.
Redis는 이벤트 루프를 돌려 커널에 등록된 수많은 소켓 중 읽기·쓰기 준비가 된 소켓만 가져와 즉시 처리합니다.
파일 디스크립터를 커널이 계속 감시하기 때문에 Redis는 별도 쓰레드로 감시할 필요 없이, 싱글 쓰레드의 이벤트 루프로 준비된 FD만 반환받아 처리합니다.
5. 빠른 이유 ③ O(1) 자료구조 (해시 테이블·스킵리스트)
세 번째 이유는 키 조회의 시간 복잡도가 $O(1)$이라는 것이다. Redis의 키는 내부적으로 전역 해시 테이블로 탐색한다. 키의 해시값을 계산해 어떤 슬롯에 있는지 바로 점프하고, 버킷이 참조하는 실제 데이터를 즉시 찾는다 — 처음부터 스캔할 필요가 없다. 해시 충돌은 체이닝으로 해결한다.
전역 키 조회뿐 아니라 Redis 내부 타입들도 최적화된 자료구조로 구현돼 대부분의 연산이 상수 시간에 끝난다. 대표적으로 Sorted Set은 보통 쓰이는 레드-블랙 트리 대신 스킵리스트를 사용해, 성능은 비슷하게 유지하면서 구현은 더 단순하게 만들었다.
⚠️ antirez는 스킵리스트 선택에 대해 "성능 최적이 아니라 단순함을 선택했다"고 밝혔다. 이는 ②의 IO 설계, ④의 프로토콜 설계와 일관된 Redis의 철학으로 — 한계 성능을 짜내기보다 '충분히 빠르면서 단순한' 구조를 택하는 트레이드오프가 코드베이스 전반을 관통한다.
▶️ 관련 스크립트
세 번째 이유는 Redis의 키 조회가 시간 복잡도 1에 해당한다는 것입니다.
키는 내부적으로 해시 테이블로 탐색합니다. 키의 해시값을 계산해 어떤 슬롯에 있는지 바로 찾아가므로 시간 복잡도 1이고, 처음부터 스캔할 필요 없이 특정 슬롯으로 점프해 버킷이 참조하는 실제 데이터를 즉시 탐색합니다.
시간 복잡도 1의 자료구조를 설계해 대부분의 연산이 상수 시간으로 끝나고, 내부 데이터 타입들도 최적화된 자료구조로 상수 시간을 유지합니다.
Sorted Set은 내부적으로 스킵리스트를 사용합니다. 보통 정렬된 셋은 레드-블랙 트리를 많이 쓰는데, Redis는 스킵리스트로 성능은 비슷하게, 구현은 더 단순하게 했습니다.
6. 빠른 이유 ④ RESP 프로토콜 — 길이 프리픽스 파싱
네 번째 이유는 RESP(REdis Serialization Protocol), 즉 Redis가 클라이언트와 주고받는 통신 규약이다. 파싱 비용을 극단적으로 줄여 싱글 쓰레드에서도 초당 수십만~수백만 건을 처리하게 한다. 핵심은 "길이를 먼저 알려주면 스캔할 필요가 없다"이다.
방식
동작
문제/이점
일반 텍스트 프로토콜
쉼표·스페이스 같은 구분자가 나올 때까지 한 글자씩 스캔
데이터 크기를 미리 모름 → 버퍼 사전 할당 불가 → 메모리 낭비
RESP
인자 크기·명령 길이를 먼저 명시 ($5 → 뒤 5바이트)
스캔 불필요 + 버퍼 크기 사전 확정 → 파싱 루프 단순·오버헤드 감소
예컨대 $5를 읽으면 정확히 그 뒤 패킷이 5바이트임을 알 수 있어, 일일이 스캔하지 않고 버퍼를 미리 확정한다. 초당 100만 건을 처리하려면 명령 하나하나의 파싱 시간조차 줄여야 하며, RESP는 그 목적으로 설계된 통신 규약이다.
⚠️ 길이 프리픽스 설계의 본질은 '읽기 전에 크기를 안다'는 것이다. 구분자 스캔 방식은 끝을 모른 채 읽으며 파악해야 해 버퍼를 미리 잡지 못하지만, RESP는 길이를 먼저 받아 메모리 할당과 파싱을 한 번에 결정한다 — 이는 직렬화 프로토콜 설계의 일반 원리다.
▶️ 관련 스크립트
네 번째 이유는 RESP 프로토콜입니다. REdis Serialization Protocol의 약자로, Redis가 클라이언트와 주고받는 통신 규약입니다.
핵심은 '길이를 먼저 알려주면 스캔할 필요가 없다'입니다.
일반 방식은 쉼표 같은 구분자가 나올 때까지 한 글자씩 스캔해야 합니다. 데이터 크기를 모르니 버퍼를 미리 할당할 수 없어 메모리 낭비가 발생합니다.
RESP는 인자 크기와 다음 명령의 길이를 먼저 알려줍니다. 예를 들어 '$5'를 읽으면 정확히 그 뒤 패킷이 5바이트인 걸 알게 됩니다.
그래서 일일이 스캔할 필요 없이 버퍼 크기를 미리 확정할 수 있고, 파싱 루프가 단순해지고 오버헤드가 감소합니다.
7. 빠른 이유 ⑤ 파이프라인과 배치 처리
다섯 번째 이유는 파이프라인이다. 여러 명령을 한 번에 묶어 전달하는 기능으로, 명령마다 응답을 주고받을 때 발생하는 네트워크 왕복 시간(RTT)을 제거한다. 파이프라인 없이는 명령→응답→명령→응답으로 RTT가 매번 추가되지만, 파이프라인은 여러 명령을 모아 한 번에 전달·실행하고 결과를 한 번에 반환한다. 공식 벤치마크상 파이프라인 미사용 시 초당 약 10만 건이 사용 시 약 180만 건으로 뛴다.
⚠️ 파이프라인의 이득은 연산 속도가 아니라 네트워크 왕복 횟수를 줄이는 데서 온다. 즉 명령 처리 자체는 이미 빠르므로, 다수 명령을 보낼 때 진짜 병목은 RTT이고 파이프라인은 그 왕복을 N회에서 1회로 줄이는 배치 전략이다.
📝 적용 예시
RTT 절감 예시: 네트워크 지연(RTT)이 1ms인 환경에서 명령 100개를 단건으로 전송 → 왕복 100회 → 100ms 이상 소요. 같은 100개를 파이프라인으로 묶어 한 번에 전송 → 왕복 1회 → 약 1ms로 단축.
▶️ 관련 스크립트
다섯 번째 이유는 파이프라인과 배치 처리입니다. 여러 명령을 한 번에 묶어 전달하는 기능입니다.
벤치마크상 파이프라인을 쓰지 않으면 초당 10만 건 정도지만, 파이프라인을 쓰면 초당 180만 건 처리가 가능합니다.
파이프라인을 쓰지 않으면 명령 하나 주고 응답받고 또 명령하고 응답받으며 RTT(네트워크 왕복 시간)가 매번 추가됩니다. 파이프라인은 여러 명령을 모아 한 번에 전달하고 결과를 한 번만 반환해 RTT를 낮춥니다.
예를 들어 RTT가 1ms인 환경에서 100개 명령을 날리면 100ms 이상 걸리는데, 파이프라인으로 묶어 한 번에 전달하면 1ms로 끊을 수 있습니다.
8. 멀티스레드는 어디에 쓰이나 (4.0 bio / 6.0 IO 쓰레드)
Redis는 사실 내부적으로 멀티스레드를 일부 지원한다. 단, 명령 실행은 여전히 싱글 쓰레드다.
버전
추가된 쓰레드
역할
초기
메인 쓰레드 1개
소켓 IO·파싱·실행·응답을 전부 처리
4.0
bio 백그라운드 쓰레드(bio.c)
비동기 삭제, AOF 파일 fsync 등 느린 디스크 작업을 분리 → 대량 삭제 시에도 메인 쓰레드 블로킹 없음
6.0
IO 쓰레드(N개, 선택 옵션)
네트워크 읽기·쓰기를 메인 쓰레드에서 분리
IO 쓰레드는 클라이언트가 많거나 응답 크기가 클 때 처리량을 30~60% 올릴 수 있다. 하지만 클라이언트가 적거나 명령이 가벼우면 오히려 쓰레드 컨텍스트 비용 때문에 느려질 수 있다. 그래서 'Redis는 싱글 쓰레드'라는 말의 정확한 의미는 명령 실행 경로가 싱글이라는 것이지, 프로세스 전체가 싱글이라는 뜻이 아니다.
⚠️ 멀티스레드가 도입된 영역은 명령 실행이 아니라 '주변부'(디스크 작업, 네트워크 IO)다. IO 쓰레드는 항상 이득이 아니라 워크로드 의존적이다 — 무거운 응답엔 도움이 되지만 가벼운 명령엔 컨텍스트 스위칭 비용이 이득을 넘어설 수 있어, 켜는 것 자체가 튜닝 판단이다.
📝 적용 예시
IO 쓰레드 설정 가이드(권장값): IO 쓰레드는 보통 CPU 코어 수보다 1~2개 작게 설정 → 8코어 서버라면 6 또는 7로 시작 → 실제 벤치마크로 최적값을 탐색.
▶️ 관련 스크립트
Redis는 내부적으로 멀티스레드를 일부 지원합니다. 다만 명령 실행은 여전히 싱글 쓰레드입니다.
Redis 4.0에서 백그라운드 쓰레드 하나를 추가했습니다. 메인 쓰레드는 명령을 실행하되, bio.c 쓰레드가 비동기 삭제나 AOF 파일 동기화를 처리해 느린 삭제·디스크 작업을 분리했습니다.
Redis 6.0에서는 소켓 IO를 처리하는 IO 쓰레드를 N개 설정할 수 있는 선택 옵션을 줬습니다. 네트워크 읽기·쓰기를 메인 쓰레드에서 떼어 별도 쓰레드로 처리합니다.
클라이언트가 많거나 응답 크기가 클 때 IO 쓰레드를 추가하면 처리량이 30~60% 올라갈 수 있습니다. 반대로 클라이언트가 적거나 명령이 가벼우면 컨텍스트 비용 때문에 오히려 느려질 수 있습니다.
'Redis는 싱글 쓰레드'의 정확한 의미는 명령 실행 경로가 싱글 쓰레드라는 것이지, 프로세스 전체가 싱글 쓰레드라는 뜻은 아닙니다.
9. [마무리 정리] 5가지 이유와 Redis가 느려지는 경우
화자가 직접 정리한 '싱글 쓰레드인데 왜 빠른가'의 다섯 가지 이유:
#
이유
한 줄 요약
1
인메모리
RAM이 디스크보다 최대 10만 배 빠름
2
IO 멀티플렉싱
epoll로 준비된 소켓만 즉시 처리
3
$O(1)$ 자료구조
해시 테이블로 대부분 연산이 상수 시간
4
RESP 프로토콜
첫 바이트로 길이 결정, 스캔 불필요
5
파이프라인
명령을 묶어 RTT 비용 절감
여기에 싱글 쓰레드라 락이 없고, 락이 없어 대기도 없으며, 그래서 단순하다는 이점이 더해진다.
반대로 Redis가 느려지는 원인은 쓰레드 수가 아니라, ① 시간 복잡도 $O(N)$ 명령에서 N이 클 때(예: KEYS), ② 특정 키 하나의 값이 매우 커진 빅 키를 한 번에 불러와 연산할 때, ③ 핫 키 — 특정 키에 요청이 몰릴 때다. 싱글 쓰레드에서는 큰 N 연산이 다른 요청을 막으므로, 무거운 연산을 항상 조심해야 한다.
⚠️ 이 정리는 영상 전체의 복습 핵심이다. 느림의 진단을 '싱글 쓰레드'로 돌리는 흔한 오해를 바로잡는 것이 결론 — 원인은 쓰레드 수가 아니라 O(N) 연산·빅 키·핫 키이며, 이는 싱글 쓰레드 모델을 이해한 사람만이 올바로 대응할 수 있다.
▶️ 관련 스크립트
정리하겠습니다. Redis가 싱글 쓰레드인데 왜 빠른가.
첫 번째는 인메모리입니다. RAM이 디스크보다 최대 10만 배 빠릅니다.
참고로 Redis가 느려지는 것은 싱글 쓰레드 때문이 아닙니다.
KEYS처럼 시간 복잡도 N에 해당하고 N이 클 경우, 싱글 쓰레드는 그 N을 처리하는 동안 다른 요청을 처리할 수 없습니다.
그리고 어떤 키 하나의 값이 굉장히 커진 빅 키를, 한 번에 다 불러와 연산할 때 느려집니다. 이 역시 N이 큰 상황입니다.
개발 가이드라인
Redis가 느릴 때 쓰레드 수를 의심하기 전에 O(N) 명령(예: KEYS)·빅 키·핫 키를 먼저 점검하라. 싱글 쓰레드에서 무거운 연산 하나가 전체 요청을 막는다.
프로덕션에서 전체 키를 훑는 KEYS 대신 커서 기반 SCAN을 쓰고, 값이 비대해지는 빅 키는 분할 설계로 예방하라.
다수 명령을 보낼 때는 파이프라인으로 묶어 RTT를 N회에서 1회로 줄여라 — 특히 네트워크 지연이 큰 환경에서 처리량이 극적으로 오른다.
멀티코어를 활용하려면 명령 실행을 병렬화하려 들기보다, 여러 Redis 인스턴스로 수평 확장(샤딩)하라.
IO 쓰레드(6.0+)는 클라이언트가 많고 응답이 클 때만 켜고, CPU 코어 수보다 1~2개 작게 설정한 뒤 벤치마크로 검증하라. 가벼운 워크로드에선 오히려 느려질 수 있다.
인메모리의 휘발성을 보완하려면 요구되는 내구성 수준에 맞춰 RDB 스냅샷과 AOF 로그를 구성하라.