서적/실전 Redis

Chapter 03.고급 기능 [Part1 기초]

Mo_bi!e 2025. 6. 9. 22:57

I. 파이프라인

필요성
Redis는 기본적으로 요청-응답 구조이기 때문에 여러 명령을 순차적으로 실행하면 RTT(Round Trip Time) 오버헤드가 발생함
파이프라인은 이러한 불필요한 왕복 비용을 줄여 다수의 명령을 빠르게 처리할 수 있도록 도와줌

문제점
명령 간 의존성이 있을 경우 처리 순서에 따른 예외가 발생할 수 있고, 응답이 몰려오기 때문에 디버깅이나 실패 추적이 어려움
또한 원자성은 보장되지 않음

1. 파이프라인(Pipelining)이란?

  • 여러 Redis 명령을 클라이언트에서 한꺼번에 전송하고, 서버는 응답을 순차적으로 반환
  • 각 명령을 RTT(Round Trip Time)마다 보내지 않고, 한 번에 요청하고 한 번에 응답 받음
  • 성능 최적화 측면에서 매우 유용 (컨텍스트 스위칭, 시스템 콜 감소)

(1) 특징

  • 명령어는 Redis 서버에서 순서대로 처리됨
  • 파이프라인 사용 시에도 Redis는 여전히 싱글스레드로 처리하므로 동시성 문제가 발생하지 않음
  • 단, 명령어 간 의존성(예: SET 후 GET) 이 있는 경우에는 적절히 분리 필요

1) 실행 예시

  • redis-cli --pipe 명령어나 Netcat(nc)을 통해 사용 가능
  • 예: 100개의 INCR 명령을 하나의 파이프로 보내는 스크립트를 작성하면 매우 빠르게 처리됨
  • redis-benchmark에서도 pipeline 설정 가능 (-P 옵션)
  • 파이프라인은 트랜잭션과 다르게 원자성 보장 X, 하지만 병렬 처리보다 빠름

II. 루아

필요성
복잡한 조건 분기나 여러 명령어를 조합해 실행할 때 클라이언트-서버 간 여러 번 왕복하면 성능 저하 발생
Lua 스크립트를 이용하면 복잡한 로직을 Redis 서버 내에서 원자적으로 실행할 수 있어 효율적임

문제점
스크립트가 무한 루프에 빠지면 Redis 서버 전체가 영향을 받을 수 있으며, 샌드박스 환경의 제한으로 Lua 내 일부 기능은 사용 불가
또한 복잡한 로직은 유지보수와 디버깅이 어려울 수 있음

1. 이페머럴 스크립트

  • 레디스 7.0 이전
  • 임시 Lua 스크립트EVAL 명령으로 실행
  • 스크립트는 매번 문자열로 전달되며, 매번 실행됨
  • 예시
EVAL "return redis.call('GET', KEYS[1])" 1 mykey
  • KEYS, ARGV 배열로 외부 인자를 전달받음
  • 실행은 원자적으로 처리되며, 중간에 다른 명령이 끼어들 수 없음

(1) 데이터 저장 + TTL 설정 (원자적으로)

HMSET은 TTL을 지정할 수 없으므로 Lua로 해결

EVAL "
  redis.call('HMSET', KEYS[1], ARGV[1], ARGV[2])
  redis.call('EXPIRE', KEYS[1], ARGV[3])
" 1 user:1 age 30 86400

한 번의 명령으로 데이터 저장과 TTL 설정을 원자적으로 처리

(2) 여러 키에 값 저장 + 서로 다른 TTL 부여

EVAL "
  redis.call('SET', KEYS[1], ARGV[1])
  redis.call('EXPIRE', KEYS[1], ARGV[2])
  redis.call('SET', KEYS[2], ARGV[3])
  redis.call('EXPIRE', KEYS[2], ARGV[4])
" 2 k1 k2 v1 60 v2 120

MSET과 달리 각 키마다 TTL을 다르게 설정

(3) 조건 분기 + TTL

EVAL "
  if redis.call('EXISTS', KEYS[1]) == 0 then
    redis.call('SET', KEYS[1], ARGV[1])
    redis.call('EXPIRE', KEYS[1], ARGV[2])
    return 'OK'
  else
    return 'EXISTS'
  end
" 1 session:user abc123 3600

조건부 저장 + TTL, 분기 로직을 Redis 서버 안에서 실행

(4) 복잡한 계산 로직 실행

EVAL "
  local sum = 0
  for i = 1, tonumber(ARGV[1]) do sum = sum + i end
  return sum
" 0 10

(5) SCRIPT LOAD + EVALSHA

SCRIPT LOAD "return {KEYS[1], ARGV[1]}"

→ SHA1 반환 후 EVALSHA로 재사용

2. 레디스 함수 (Redis Functions)

(1) 개요

  • Redis 7.0부터 도입된 기능으로, EVAL의 단점을 보완
  • 스크립트를 Redis 서버에 등록해 재사용 가능
  • 서버 재시작 후에도 유지되며, 보안·성능·관리성 향상
  • 함수는 라이브러리 단위로 그룹화되어 관리됨

1) 등록 방법

FUNCTION LOAD "<Lua 스크립트 문자열>"
-- Lua 예시
redis.register_function{
  function_name = 'echo_key',
  callback = function(keys, args)
    return keys[1]
  end
}

2) 호출 방법

FCALL echo_key 1 mykey
  • FCALL <함수명> <numkeys> <key1> <key2> ... <arg1> <arg2> ...

3) 관리 명령어

명령어 설명
FUNCTION LIST 등록된 함수 목록 확인
FUNCTION DELETE <라이브러리> 라이브러리 단위 삭제
FUNCTION FLUSH 전체 삭제
FUNCTION DUMP / RESTORE 백업 및 복구 가능

4) 특징 요약

  • 등록된 함수는 재사용 가능하고, 서버가 재시작돼도 지속됨
  • KEYS / ARGV 인자 구조는 EVAL과 동일
  • 함수는 라이브러리 단위로 등록되며, 각각의 함수는 callback으로 정의됨
  • 샌드박스 환경에서 실행되므로 외부 접근 제한됨

5) EVAL과의 차이점

항목 이페머럴 스크립트 (EVAL) Redis 함수 (FUNCTION)
실행 방식 매번 코드 전송 등록 후 함수명으로 호출
유지성 휘발성 영속성
적합 용도 단발 실행 반복 호출
관리 기능 없음 LIST, DELETE 등 제공

3. 레디스의 루아 프로그래밍

(1) 개요

  • Redis에서 Lua 스크립트는 redis.call() 또는 redis.pcall() 로 Redis 명령을 호출
  • 외부에서 전달된 값은 KEYS[]ARGV[] 배열을 통해 접근
  • Lua는 가볍고 간결한 스크립트 언어로, Redis 서버 내에서 원자적 로직 실행에 적합

1) 명령 실행 방식

  • redis.call()
    • Redis 명령 실행, 실패 시 에러 반환 → 스크립트 중단
  • redis.pcall()
    • Redis 명령 실행, 실패 시 nil 또는 오류 메시지 반환 → 스크립트 계속 진행

예:

local exists = redis.call('EXISTS', KEYS[1])
local res = redis.pcall('GET', 'not-exist-key')

2) 변수 사용 및 스코프

  • Lua는 기본적으로 전역 변수로 선언됨
  • 반드시 local 키워드로 지역 변수로 지정해야 안전
    예:
local count = redis.call('GET', KEYS[1])

3) 흐름 제어

  • 조건문
if redis.call('EXISTS', KEYS[1]) == 0 then
  redis.call('SET', KEYS[1], ARGV[1])
end
  • 반복문
local sum = 0
for i = 1, tonumber(ARGV[1]) do
  sum = sum + i
end
return sum

4) 반환 값 처리

  • return을 사용하여 단일 값, 배열, 테이블 등 반환 가능
    예:
return {KEYS[1], ARGV[1]}

5) 주의 사항

  • Redis는 Lua 5.1 버전을 사용 → 최신 Lua 기능 일부 없음
  • 스크립트에서 Redis 외부 네트워크 접근 불가 (샌드박스 환경)
  • 무한 루프, 너무 긴 실행 시간 금지 → Redis 전체가 블로킹됨
  • 반환 값은 반드시 Redis가 직렬화 가능한 타입이어야 함 (문자열, 숫자, 리스트 등)

III. 트랜잭션

1. 트랜잭션이란?

  • Redis의 트랜잭션은 여러 명령을 묶어서 순차적으로 실행하는 기능
  • MULTI로 트랜잭션 시작 → 명령들을 큐에 저장 → EXEC로 실행
  • 기본적으로 원자성EXEC 단위에서만 적용됨

(1) 트랜잭션 명령어 구조

MULTI
SET key1 value1
INCR counter
EXEC
  • MULTI: 트랜잭션 시작
  • EXEC: 큐에 저장된 명령을 순차적으로 실행
  • DISCARD: 트랜잭션 취소 (큐 비움)

1) 특성

  • Redis는 명령어들을 그대로 순서대로 실행
  • 트랜잭션 실행 중 일부 명령이 실패해도 롤백되지 않음
    → 실패한 명령은 에러, 나머지는 실행됨
  • 예외: WATCH를 사용한 낙관적 락이 있는 경우, 조건 불충족 시 전체 EXEC실행되지 않음

2) 낙관적 락 (WATCH)

  • WATCH는 지정한 키가 EXEC 전까지 변경되지 않으면 실행
  • 키가 바뀌면 EXEC 자체가 무시됨 (abort)
  • 낙관적 락 (CAS, Compare-And-Set)과 유사한 동작 방식

예:

WATCH balance
MULTI
DECR balance
EXEC

balance 키가 다른 클라이언트에 의해 변경되면 EXEC은 실행되지 않음

3) 사용 시 주의점

  • Redis는 트랜잭션 중간에 명령을 실행하지 않기 때문에, 중간 결과 확인 불가
  • 모든 명령은 EXEC 시점에 한꺼번에 실행되며, 실패해도 부분 실행이 남을 수 있음
  • 복잡한 조건 분기, 반복문, TTL 설정 등은 불가능
    → 이 경우 루아 스크립트 사용 권장

4) 트랜잭션 vs 루아 비교

항목 트랜잭션 (MULTI/EXEC) 루아 스크립트 (EVAL)
실행 방식 명령 큐 저장 후 실행 Lua로 전체 로직 실행
조건 분기 불가능 가능
부분 실패 시 롤백 안 됨 안 됨 (그러나 원자적)
사용 용도 단순한 명령 묶음 복잡한 로직, 조건 처리

IV. 모듈

1. 모듈이란?

  • Redis는 기본적으로 제공되는 명령어와 자료형 외에, C 언어 기반 사용자 정의 확장 기능을 제공함
  • 이를 모듈(module)이라 하며, 새로운 자료구조, 명령어, 기능을 직접 구현해 Redis 서버에 로드할 수 있음
  • Redis 4.0 이상에서 지원

(1) 모듈로 구현할 수 있는 것

  • 새로운 자료형 (예: Bloom Filter, Time Series 등)
  • 새로운 명령어 (예: my.hello, ts.insert, bf.add)
  • Redis 클러스터 연동, 자동 만료 로직 등 내부 동작 확장

1) 모듈 작성 개요

  • C 언어로 작성
  • Redis에서 제공하는 API 사용 (RedisModule_Init, RedisModule_CreateCommand, 등)
  • .so 공유 라이브러리로 컴파일 후 Redis에 로드

2) 로드 방법

MODULE LOAD /path/to/mymodule.so
  • Redis 시작 시 자동 로드하려면 redis.conf에 loadmodule 옵션 추가
  • 로드된 모듈은 Redis 내부 명령처럼 동작

3) 예시: 단순 명령어 출력

int HelloCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
  return RedisModule_ReplyWithSimpleString(ctx, "Hello from module!");
}

int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
  RedisModule_Init(ctx, "mymodule", 1, REDISMODULE_APIVER_1);
  RedisModule_CreateCommand(ctx, "mymodule.hello", HelloCommand, "readonly", 0, 0, 0);
  return REDISMODULE_OK;
}
  • 이 코드는 mymodule.hello라는 커맨드를 만들어, 호출 시 "Hello from module!"을 반환
  • 실전에서는 RedisTimeSeries, RedisBloom, RedisSearch 같은 복잡한 기능도 모듈로 구현됨

4) 주의 사항

  • Redis 프로세스 내에서 실행되므로 심각한 버그는 서버 전체에 영향을 줄 수 있음
  • C로 직접 구현해야 하므로 진입 장벽이 있음
  • 성능과 안정성 테스트가 중요

5) 실무 활용 예시

대표 모듈 기능
RedisBloom Bloom Filter, Count-Min Sketch 등
RedisTimeSeries 시계열 데이터 저장/집계
RedisSearch 텍스트 검색, 인덱스
RedisJSON JSON 구조 저장 및 조회

V. 키 공간 알림

1. 키 공간 알림이란?

  • Redis에서 특정 키에 대해 발생하는 이벤트(예: 설정, 삭제, 만료 등)를 Pub/Sub 메시지 형태로 알림해주는 기능
  • 일반적으로 클라이언트가 특정 키의 변화를 실시간으로 감지하거나 동기화할 때 사용됨
  • Redis 2.8부터 지원

(1) 동작 방식

  • Redis는 특정 키의 이벤트 발생 시 특수 채널에 메시지 발행
  • 클라이언트는 해당 채널을 SUBSCRIBE 또는 PSUBSCRIBE로 구독
  • 알림 채널은 두 종류
    • __keyspace@<db번호>__:<key> → 명령 이름을 발행 (예: "set", "del")
    • __keyevent@<db번호>__:<event> → 이벤트가 발생한 키를 발행

예:

PSUBSCRIBE __keyevent@0__:expired

→ DB 0에서 만료된 키의 이름을 실시간 수신

1) 설정 방법

  • 기본적으로 Redis는 키 공간 알림을 비활성화함
  • 다음과 같이 설정해야 함 (런타임/설정파일 모두 가능)
CONFIG SET notify-keyspace-events Ex
  • 옵션 조합 예시
    • K → Keyspace notification
    • E → Keyevent notification
    • g → generic (set, del 등)
    • x → expired
    • A → all

2) 활용 예시

  • 만료된 세션 캐시 감지 후 정리
    • expired 이벤트로 세션 만료 알림을 수신해 후처리
  • 키 변경 감지
    • set, del, rename 등의 이벤트를 구독하여 실시간 반영
  • 분산 캐시 무효화
    • 여러 노드 간 캐시를 일관되게 유지하려고 알림 기반 invalidation 처리

3) 주의 사항

  • 키 이벤트는 Pub/Sub 기반이라 클라이언트가 반드시 구독 중이어야 함
  • 과도한 이벤트 설정은 성능 저하 유발 가능 (예: A 옵션)
  • 일시적인 네트워크 오류로 이벤트를 놓칠 수 있음 → 신뢰성 보장되지 않음

VI. 클라이언트 측 캐시

1. 클라이언트 측 캐시란?

  • Redis 6부터 도입된 기능으로, 클라이언트가 Redis 데이터를 로컬에 캐싱하고, 서버에서 변경 알림을 받아 자동 무효화(invalidation) 하는 방식
  • 기존에는 클라이언트 캐시가 수동 갱신 또는 TTL 기반이었지만, 이 기능을 통해 서버 주도 캐시 무효화가 가능해짐

(1) 동작 방식

  • 클라이언트가 CLIENT TRACKING 명령으로 추적 모드를 활성화
  • 이후 읽은 키들에 대해 Redis가 변경 이벤트 발생 시 무효화 메시지를 전송
  • 클라이언트는 해당 키의 로컬 캐시를 무효화하거나 재요청

1) 기본 사용 방식

CLIENT TRACKING ON
GET mykey

→ 이후 mykey가 변경되면 Redis는 클라이언트에게 invalidation 메시지를 전송

  • 클라이언트는 이를 기반으로 로컬 캐시 무효화 처리

2) 주요 옵션

옵션 설명
ON 추적 활성화
BCAST 모든 키에 대해 브로드캐스트 모드 사용
REDIRECT <id> 지정한 Pub/Sub 클라이언트 ID로 무효화 전송
PREFIX <prefix> 특정 접두어 키만 추적
NOLOOP 자기 자신의 명령으로 인한 무효화는 제외

예:

CLIENT TRACKING ON BCAST PREFIX foo: NOLOOP

3) 활용 사례

  • API Gateway 캐싱
    • 자주 조회되는 값을 Redis에서 조회 후 클라이언트에 캐시하고, 서버에서 실시간 무효화 전송
  • 게임 서버나 실시간 UI 캐시
    • 플레이어 상태, 랭킹 등 조회 수요가 높은 데이터에 적용
  • 분산 캐시 무효화 대체재
    • 키 공간 알림보다 정밀하고, 클라이언트 단위로 분리 가능

4) 주의 사항

  • 클라이언트가 무효화 메시지를 직접 처리해야 하므로 구현 복잡도 있음
  • 무효화 메시지를 받지 못하면 오래된 캐시가 유지될 수 있음
  • BCAST는 모든 키 변경을 브로드캐스트하므로 과도하게 설정 시 성능 저하 유발 가능