본문 바로가기

학습 기록 (Learning Logs)/Today I Learned

레디스 pub/sub

1. 도커 컨테이로 redis-test 를 만든다

도터 컨테이너에 redis 띄었음

docker run -d --name redis-test -p 6379:6379 redis

 

d819dab48c79ac486cba7351ece464566e2f2e42c47867b37db1d023031930b3

 

 

devwonny@devwonnyui-MacBookAir Documents % docker logs d819dab48c79

 

-> 결과

1:C 24 Mar 2025 08:20:16.830 * oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo

1:C 24 Mar 2025 08:20:16.830 * Redis version=7.4.2, bits=64, commit=00000000, modified=0, pid=1, just started

1:C 24 Mar 2025 08:20:16.830 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf

1:M 24 Mar 2025 08:20:16.830 * monotonic clock: POSIX clock_gettime

1:M 24 Mar 2025 08:20:16.831 * Running mode=standalone, port=6379.

1:M 24 Mar 2025 08:20:16.831 * Server initialized

1:M 24 Mar 2025 08:20:16.831 * Ready to accept connections tcp


2. SubscriberA : redis-cli(A) : 구독하기 mychannel

기존 터미널에서 실행(A)

docker exec -it redis-test redis-cli ---> redis-test 에 접근해서 redis-cli 실행(A)

 


3. Publisher : redis-cli(B) : 발행하기 mychannel

새로운 터미널 생성(B)

docker exec -it redis-test redis-cli

 

127.0.0.1:6379> PUBLISH mychannel "안녕 레디스!"

(integer) 1

127.0.0.1:6379> PUBLISH mychannel "?hello redis?!"

(integer) 1

127.0.0.1:6379> PUBLISH mychannel "hello redis"

 

 

3.1. SubscriberA : redis-cli(A) : 구독자는 메세지 받음

 

1) "message"

2) "mychannel"

3) "\xec\x95\x88\xeb\x85\x95 \xeb\xa0\x88\xeb\x94\x94\xec\x8a\xa4!" -> "안녕 레디스!"

 

1) "message"

2) "mychannel"

3) "\xechello redis\xa4!" -> "?hello redis?!"

 

1) "message"

2) "mychannel"

3) "hello redis" -> "hello redis"

 


4. SubscriberA : redis-cli(C) 추가 : 구독하기 mychannel

새로운 터미널 생성(C)

docker exec -it redis-test redis-cli


5. Publisher : redis-cli(B) : 발행하기 mychannel

PUBLISH mychannel "hello redis22"

 

5-1. SubscriberA : redis-cli(A) : 구독자 "hello redis22" 메세지 받음

5-2. SubscriberA : redis-cli(C) : 구독자  "hello redis22" 메세지 받음


6. 도커 컨테이너 접근

apt update && apt install -y procps
ps aux

  • redis-server (PID 1)
    • Redis 서버가 실행 중이며 포트 6379을 리스닝 중
  • redis-cli 3개
    • PID 22, 28, 33
    • 각각:
      • 하나는 publisher
      • 나머지 둘은 subscriber1, subscriber2

🧩 구조

[도커 컨테이너 하나]
┌────────────────────────────────────┐
│   redis-server (*:6379)           │
│       ├─ redis-cli (subscriber1)  │
│       ├─ redis-cli (subscriber2)  │
│       └─ redis-cli (publisher)    │
└────────────────────────────────────┘

 

 ✅ 상황 요약

  • 하나의 도커 컨테이너에서 Redis 서버 + redis-cli 3개가 동작 중
  • 즉, 같은 서버(호스트) 내에서 Redis에 접근하는 구조
  • 모든 redis-cli가 같은 Redis 서버(*:6379)에 접근 중

🚀 서버 여러 대에서의 구조 (채팅 시스템)

 ✅ 다중 채팅 서버, 고려 사항

  • 현재 도커 환경에선 한 서버 내에서의 Pub/Sub 테스트 상태
  • 실제 채팅 서버 여러 대에서 Redis Pub/Sub을 사용하려면:
    • 모든 서버 단일 Redis 서버에 연결되게 설정 -> 각 서버에 subscriber를 띄우고, 메시지를 받아 처리하면 됨
    • 각 서버 별로 redis 설치, redis cluster사용

 

 

 

 

 

 

 

이라고 생각했으나...

 

 

🔹 "모든 노드들한테 다 쏴줘야 안정성이 보장이됨"

클러스터 환경에선 메시지가 모든 노드에 BroadCast되지 않음

메시지를 모든 노드(subscriber가 있을지도 모르는 노드)에 직접 publish 해줘야 하기 때문에,

  • 자동으로 퍼지지 않음 → 안정성이 떨어짐
  • 결국 모든 노드에 수동으로 보내야 함

 

🔹 "node1-slave에 쏘면, subscribe 안한 친구들은 못 받아"

클러스터 환경에서 어떤 특정 노드(예: slave)에 메시지를 publish 해도,

  • 그 노드에 subscribe 중인 클라이언트가 없으면아무도 못 받는다 😨
  • 메시지 브로드캐스트가 안 되기 때문

 

🔹 "pub/sub만 쓸 거라면 단일노드가 맞긴 하지"

Pub/Sub 기능만 쓸 거면, 단일 Redis 서버를 사용하는 것이 더 정확하고 간단하다

  • 하나의 Redis 인스턴스에 모든 서버가 붙어서 Pub/Sub 하면 됨

 

 

🔹 "근데 장애났을 때 모든 pub/sub이 장애가 나니"

단일 Redis장애가 나면 전체 Pub/Sub이 멈춘다는 단점도 있어요.

  • 그래서 고가용성이 필요한 경우엔 더 고민이 필요해요

 

 

🔹 "여러 번 쏘면서 중복 방지하고 클러스터로 쓸래, 간단하게 그냥 쓸래 이런 걸 고려해야지"

단일 Redis로 간단하게 장애는 리스크 있지만, Pub/Sub은 안정적으로 잘 작동
Redis Cluster + 다중 Publish 고가용성은 확보할 수 있으나, 직접 메시지를 모든 노드에 보내야 하고 복잡

 

항목 단일 Redis Redis Cluster
Pub/Sub 지원 ✅ 완전 지원 ❌ 제대로 안됨
메시지 브로드캐스트 ✅ 자동 ❌ 직접 여러 노드에 쏴야 함
장애 대응 ❌ 단일 장애점 존재 ✅ 자동 Failover 가능 (하지만 Pub/Sub엔 영향 적음)
구현 난이도 😄 쉬움 😰 복잡하고 제한 많음

 

 

🔥 그래서 실제로 어떻게 해야 할까?

  • Pub/Sub만 쓸 목적이라면 → 단일 Redis + Redis Sentinel 정도로 고가용성 대응하는 게 현실적이에요
  • 정말 고가용성 + 메시지 유지 + 확장성 필요하면 → Redis Stream, 혹은 Kafka 고려

 


 

✅ 1. 모든 서버가 "단일 Redis 서버"에 연결 (가장 일반적인 방식)

🧩 구조

[서버 A] ──┐
[서버 B] ──┤── Redis 서버 (Pub/Sub 단일 인스턴스)
[서버 C] ──┘

 

👍 장점

  • 구현이 간단함
  • 메시지 전달 신뢰성 높음
  • subscriber가 여러 서버에 있어도 메시지가 모두 브로드캐스트됨

👎 단점

  • Redis 인스턴스 하나에 트래픽이 몰리면 병목 발생
  • 고가용성/스케일 아웃이 어려움 (단일 장애 지점, SPOF)

👉 이런 경우에 적합

  • 실시간 채팅, 알림 등 브로드캐스트 성격이 강할 때
  • 서버 수가 적거나 중간 정도 트래픽

 

✅ 2. 각 서버마다 Redis 설치 + Redis Cluster 구성

🧩 구조

[서버 A] ─ Redis A (Cluster Node)
[서버 B] ─ Redis B (Cluster Node)
[서버 C] ─ Redis C (Cluster Node)

 

👍 장점

  • 분산 처리 가능 (확장성)
  • Redis 자체적으로 데이터 샤딩 및 고가용성
  • 클러스터링으로 장애 대응

👎 단점

  • Pub/Sub 메시지는 클러스터 간 브로드캐스트되지 않음
    • ❗️subscriber와 publisher가 같은 노드에 있어야만 메시지를 수신함
    • 즉, 클러스터 환경에서는 Pub/Sub 제대로 작동하지 않음
  • 대신에 Redis Stream이나 Kafka를 써야 함

👉 이런 경우에 적합

  • 고가용성, 확장성이 중요한 서비스
  • 하지만 Pub/Sub 용도보단 일반 데이터 저장/조회에 더 적합
  • Pub/Sub 대신 Redis Stream, Kafka 사용을 고려해야 함

 

선택지 추천 여부 이유
단일 Redis ✅ 추천 간단하고 Pub/Sub 동작 확실
Redis Cluster ❌ 비추천 (Pub/Sub 용도로는) Pub/Sub 메시지 브로드캐스트 안됨
Redis Stream ✅ 중장기적으로 고려 소비자 그룹으로 메시지 보장, 로그 기반
Kafka ✅ 고성능/고신뢰 요구 시 메시지 내구성과 확장성 우수

🧠 Redis Cluster란?

여러 개의 Redis 노드를 묶어서 하나의 큰 Redis처럼 작동하게 하는 구조

  • 하나의 Redis 인스턴스로 감당 못할 만큼 데이터량이 많거나
  • Redis 서버에 장애가 나도 서비스를 유지하고 싶을 때
  • 여러 Redis를 자동 분산 + 고가용성으로 구성한 것

 


🧱 구조 그림 (기본적으로 6개 노드가 필요)

         ┌────────────┐
         │  Client    │
         └────┬───────┘
              │
       ┌──────┴───────┐
       ▼              ▼
┌────────────┐   ┌────────────┐
│ Redis Node │   │ Redis Node │  ... (최소 3개 Master)
│  (Master)  │   │  (Master)  │
└────┬───────┘   └────┬───────┘
     │                │
     ▼                ▼
┌────────────┐   ┌────────────┐
│ Redis Node │   │ Redis Node │  ... (각 Master의 Slave)
│  (Replica) │   │  (Replica) │
└────────────┘   └────────────┘

 

 

 

 

 


서로 IP가 다른 5개의 Redis 서버를 클러스터로 묶는 방법

 

예: 5대 서버 각각의 IP가 다음과 같다고 가정해요.

  • 192.168.0.1
  • 192.168.0.2
  • 192.168.0.3
  • 192.168.0.4
  • 192.168.0.5
  • 포트는 모두 7000 사용.

1) 각 서버에 Redis 설치 + 설정 파일 수정

redis.conf (or redis-cluster.conf)에 아래 항목 반드시 수정:

port 7000
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
bind 0.0.0.0

 

그리고 redis-server redis.conf로 각 서버에서 Redis 실행

 

2) 방화벽(보안그룹) 확인

모든 서버가 서로의 7000, 17000 포트를 열어야 함

  • Redis 데이터: 7000
  • Redis 클러스터 내부 통신: 7000 + 10000 = 17000
7000-7005 / TCP 허용
17000-17005 / TCP 허용

 

3) 클러스터 생성 명령 실행

어느 한 서버에서 실행:

redis-cli --cluster create \
192.168.0.1:7000 \
192.168.0.2:7000 \
192.168.0.3:7000 \
192.168.0.4:7000 \
192.168.0.5:7000 \
--cluster-replicas 0
--cluster-replicas 0: 단순 5개 마스터 노드 구성
복제본을 만들고 싶으면 --cluster-replicas 1 등으로 설정

 

4) 정상 작동 확인

redis-cli -c -h 192.168.0.1 -p 7000 cluster nodes
 
각 노드 ID, IP, 포트, 상태(Master/Slave)가 출력되면 성공!

 

5) 로컬에서 테스트할 경우

Docker로 5개 Redis 컨테이너 만들고, --net=host or Docker 네트워크 이용

docker network create redis-cluster-net

 


🔄 클러스터의 핵심 기능

1) 데이터 분산 저장 (Sharding)

  • Redis는 기본적으로 단일 노드에서 모든 키를 저장해요.
  • 클러스터에서는 키값 해싱 → 슬롯(16384개)노드에 분배해서 저장해요.
  • 즉, 데이터를 여러 서버에 나눠서 저장함.

2) 장애 복구 (Failover)

  • Master가 죽으면 연결된 Replica가 자동으로 승격돼요.
  • 클러스터가 알아서 장애를 감지하고 서비스 무중단 유지 가능.

3) 수평 확장

  • 노드를 더 추가하면 성능/용량이 증가.

 

⚠️ 주의할 점

  • Pub/Sub은 클러스터에서 제약이 많아요!
    • 메시지를 모든 노드에 브로드캐스트하지 않음
    • Subscriber가 있는 노드에만 메시지가 전달됨 → 실시간 알림/채팅에는 부적합

 

목적
추천 방식
간단한 캐시, Pub/Sub 기반 채팅 단일 Redis (혹은 Sentinel)
데이터량 많고, 고가용성/확장성 필요 Redis Cluster
채팅 메시지 브로드캐스트 단일 Redis or Redis Stream or Kafka

 

 

 

 

 

 


단일 Redis or Redis Stream 이 설치랑 코드가 다른가?

 

항목 Redis Pub/Sub Redis Stream
메시지 저장 ❌ 안 됨 ✅ 됨
실시간성 ✅ 아주 좋음 ✅ 좋음
장애 복구 ❌ 어려움 (메시지 유실) ✅ 메시지 유지
소비자 그룹 ❌ 없음 ✅ 있음
설치 방식 동일 동일
코드 작성 🔁 완전 다름 🔁 완전 다름
복잡도 😄 쉬움 😅 복잡함
채팅 ✅ 1:1 or 실시간 알림 ✅ 대규모 안정적인 채팅 시스템

 

사용 목적 추천 방식
간단한 실시간 채팅 or 알림 Pub/Sub
안정적 메시지 큐, 저장, 다시 처리, 멀티 서버 소비 Redis Stream

✅ 설치 측면: 거의 똑같음

Redis 자체는 설치만 하면 Pub/Sub도 가능하고, Stream도 기본 제공돼요.

docker run -d --name redis -p 6379:6379 redis:latest

 

  • PUBLISH, SUBSCRIBE 가능 (Pub/Sub)
  • XADD, XREAD, XGROUP 가능 (Stream)

✅ 코드 측면: 완전히 다름

🔹 단일 Redis + Pub/Sub 방식

👉 개념

  • 실시간 메시지 전송
  • 메시지를 받는 subscriber가 실시간으로 살아 있어야 함
  • 메시지를 Redis가 저장하지 않음

 

PUBLISH chat-room-1 "hello"
SUBSCRIBE chat-room-1


// 발행
redisTemplate.convertAndSend("chat-room-1", "hello");

// 구독
@RedisMessageListener(topics = "chat-room-1")
public void handleMessage(String message) {
    System.out.println("수신 메시지: " + message);
}

 

 

 

🔹 Redis Stream 방식

👉 개념

  • 메시지 저장 가능 (로그처럼)
  • 나중에 읽거나, 소비자 그룹별로 처리 가능
  • subscriber가 꺼져 있어도 메시지 손실 없음
XADD mystream * message "hello"
XREAD COUNT 1 STREAMS mystream 0

// 메시지 추가
Map<String, String> message = Map.of("user", "wonny", "text", "hello");
redisTemplate.opsForStream().add("chat-stream", message);

// 메시지 읽기
List<MapRecord<String, Object, Object>> messages = redisTemplate.opsForStream()
    .read(StreamReadOptions.empty(), StreamOffset.create("chat-stream", ReadOffset.latest()));

 

 

 

Redis Stream = Redis 안에 브로커 역할을 하는 데이터 구조

  • 별도의 Kafka처럼 독립된 브로커가 있는 건 아니고,
  • Redis 서버 자체가 메시지를 저장하는 브로커처럼 동작해요.

즉, Redis에 XADD로 메시지를 추가하면, 그게 일종의 로그(append-only stream) 으로 쌓이고, 각 서버(=consumer)는 거기서 XREAD, XREADGROUP으로 메시지를 가져가는 구조예요.

 

📦 구조 예시

                 ┌────────────┐
 [Producer 1]───▶│            │
                 │            │
 [Producer 2]───▶│ Redis      │◀── [Consumer Group A]
                 │ Stream     │◀── [Consumer Group B]
 [Producer 3]───▶│ (XADD)     │
                 └────────────┘

 

 

 

✅ Redis Stream이 Pub/Sub과 다른 핵심 포인트

항목 Redis Pub/Sub Redis Stream
메시지 저장 ❌ 안 됨 ✅ 됨 (디스크에도)
메시지 재시도 ❌ 불가 ✅ 가능
메시지 누락 방지 ❌ (실시간 안 받으면 끝) ✅ 안 읽은 거 다 있음
소비자 그룹 ❌ 없음 ✅ 있음 (XGROUP)
서버 여러 대 처리 ⚠️ 브로드캐스트 서버 여러 대가 분산해서 소비 가능

 

🚀 서버 여러 대에서 어떻게 사용하나요?

Redis Stream핵심은 소비자 그룹 (Consumer Group) 이에요.

Stream 이름: chat-messages
Consumer Group: chat-group

서버 A → consumer name: chat-a-1
서버 B → consumer name: chat-b-1
서버 C → consumer name: chat-c-1

 

이렇게 설정하면 Redis가 알아서 메시지를 분산 처리해줘요.

  • 각 서버는 XREADGROUP을 사용해 자기 순서대로 메시지를 가져감
  • 메시지 누락 없이 안정적으로 처리
  • 장애가 나면 pending 메시지 다시 확인해서 재시도 가능
//1. 메시지 추가 (생산자)
Map<String, String> msg = Map.of("user", "wonny", "text", "hello");
redisTemplate.opsForStream().add("chat-messages", msg);

//2. 메시지 읽기 (소비자 그룹)
StreamReadOptions options = StreamReadOptions
					.empty()
					.count(1)
					.block(Duration.ofSeconds(2));

StreamOffset<String> offset = StreamOffset
			.create("chat-messages", ReadOffset.lastConsumed());

List<MapRecord<String, Object, Object>> messages =
    redisTemplate.opsForStream()
    .read(Consumer
    .from("chat-group", "server-A-1"), options, offset);

 

 

Redis Stream의 진짜 강점

  • 메시지 유실 거의 없음
  • 서버 여러 대가 동시에 안전하게 메시지 처리 가능
  • Kafka까지는 필요 없지만 Pub/Sub보다 강력한 보장성이 필요한 시스템에 적합
  • 예: 채팅, 알림, 작업 큐 등