Contents
see ListRedis는 2024년 하반기 라이선스 변경 논란을 거쳤지만, 여전히 인메모리 데이터 스토어의 사실상 표준이다. Redis 7.x에서 추가된 Function, Client-side Caching과 함께, 실전에서 효과적인 캐싱 전략을 종합적으로 정리한다.
1. Redis Functions (Lua Script 대체)
Redis 7에서 도입된 Functions는 기존 Lua Script를 대체하는 서버 사이드 프로그래밍 모델이다. 라이브러리 형태로 관리되어 재사용성과 가독성이 크게 향상되었다.
#!lua name=mylib
-- 조회수 증가 + 랭킹 업데이트를 원자적으로 처리
local function increment_with_ranking(keys, args)
local article_key = keys[1]
local ranking_key = keys[2]
local article_id = args[1]
-- 조회수 증가
local views = redis.call('HINCRBY', article_key, 'views', 1)
-- 랭킹 Sorted Set 업데이트
redis.call('ZADD', ranking_key, views, article_id)
-- 상위 10개만 유지
redis.call('ZREMRANGEBYRANK', ranking_key, 0, -11)
return views
end
-- 함수 등록
redis.register_function('increment_with_ranking', increment_with_ranking)
# 함수 라이브러리 로드
cat mylib.lua | redis-cli -x FUNCTION LOAD REPLACE
# 함수 호출
redis-cli FCALL increment_with_ranking 2 article:123 ranking:daily art123
# 등록된 함수 목록
redis-cli FUNCTION LIST
# 함수 덤프/복원 (클러스터 간 이동)
redis-cli FUNCTION DUMP > functions.rdb
redis-cli FUNCTION RESTORE < functions.rdb
2. 실전 캐싱 패턴
Cache-Aside (Lazy Loading)
// Node.js + ioredis 예시
import Redis from 'ioredis';
const redis = new Redis({ host: 'localhost', port: 6379 });
async function getUser(userId) {
const cacheKey = `user:${userId}`;
// 1. 캐시 조회
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// 2. DB 조회
const user = await db.users.findById(userId);
if (!user) return null;
// 3. 캐시 저장 (TTL 1시간)
await redis.setex(cacheKey, 3600, JSON.stringify(user));
return user;
}
// 업데이트 시 캐시 무효화
async function updateUser(userId, data) {
await db.users.update(userId, data);
await redis.del(`user:${userId}`); // 캐시 삭제
}
Write-Through Cache
// 쓰기와 캐시를 동시에 업데이트
async function saveProduct(product) {
const pipeline = redis.pipeline();
// DB 저장
const saved = await db.products.save(product);
// 캐시 동시 업데이트
pipeline.setex(
`product:${saved.id}`,
7200,
JSON.stringify(saved)
);
// 카테고리별 목록 캐시도 갱신
pipeline.sadd(`category:${saved.categoryId}:products`, saved.id);
pipeline.expire(`category:${saved.categoryId}:products`, 7200);
await pipeline.exec();
return saved;
}
3. Redis Streams 실전 활용
Redis Streams는 Apache Kafka의 경량 대안으로, 이벤트 소싱과 메시지 큐에 적합하다.
# 이벤트 발행
XADD events:orders * action created orderId ORD-001 amount 89000
XADD events:orders * action paid orderId ORD-001 method card
XADD events:orders * action shipped orderId ORD-001 carrier cj
# 컨슈머 그룹 생성
XGROUP CREATE events:orders order-processors $ MKSTREAM
# 컨슈머가 메시지 읽기
XREADGROUP GROUP order-processors worker-1 COUNT 10 BLOCK 5000 STREAMS events:orders >
# 처리 완료 확인 (ACK)
XACK events:orders order-processors 1234567890-0
# 미처리 메시지 확인 (Pending)
XPENDING events:orders order-processors - + 10
# 스트림 길이 제한 (최근 10만 건만 유지)
XTRIM events:orders MAXLEN ~ 100000
// Node.js에서 Stream Consumer 구현
async function startConsumer() {
const groupName = 'order-processors';
const consumerName = `worker-${process.pid}`;
const streamKey = 'events:orders';
// 컨슈머 그룹 생성 (이미 있으면 무시)
try {
await redis.xgroup('CREATE', streamKey, groupName, '$', 'MKSTREAM');
} catch (e) {
// BUSYGROUP - 이미 존재
}
while (true) {
const results = await redis.xreadgroup(
'GROUP', groupName, consumerName,
'COUNT', 10,
'BLOCK', 5000,
'STREAMS', streamKey, '>'
);
if (!results) continue;
for (const [stream, messages] of results) {
for (const [id, fields] of messages) {
const event = Object.fromEntries(
fields.reduce((acc, val, i) => {
if (i % 2 === 0) acc.push([val, fields[i + 1]]);
return acc;
}, [])
);
console.log(`처리 중: ${id}`, event);
await processEvent(event);
await redis.xack(streamKey, groupName, id);
}
}
}
}
4. 분산 락 (Distributed Lock)
// Redlock 패턴 구현
async function acquireLock(lockKey, ttlMs = 10000) {
const lockValue = crypto.randomUUID();
const acquired = await redis.set(
`lock:${lockKey}`,
lockValue,
'PX', ttlMs, // 밀리초 단위 TTL
'NX' // 키가 없을 때만 설정
);
return acquired === 'OK' ? lockValue : null;
}
async function releaseLock(lockKey, lockValue) {
// Lua 스크립트로 원자적 해제 (본인이 획득한 락만 해제)
const script = `
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end
`;
return await redis.eval(script, 1, `lock:${lockKey}`, lockValue);
}
// 사용 예시: 재고 차감
async function deductStock(productId, quantity) {
const lockValue = await acquireLock(`stock:${productId}`, 5000);
if (!lockValue) throw new Error('재고 처리 중입니다. 잠시 후 다시 시도하세요.');
try {
const stock = parseInt(await redis.get(`stock:${productId}`));
if (stock < quantity) throw new Error('재고 부족');
await redis.decrby(`stock:${productId}`, quantity);
} finally {
await releaseLock(`stock:${productId}`, lockValue);
}
}
5. Redis 성능 모니터링
# 실시간 명령어 모니터링
redis-cli MONITOR
# 메모리 분석
redis-cli INFO memory
redis-cli MEMORY DOCTOR
redis-cli MEMORY USAGE user:12345
# 느린 명령어 로그
redis-cli SLOWLOG GET 10
redis-cli CONFIG SET slowlog-log-slower-than 10000 # 10ms 이상
# 키 분석
redis-cli --bigkeys # 큰 키 찾기
redis-cli --memkeys # 메모리 많이 사용하는 키
# Latency 모니터링
redis-cli --latency
redis-cli --latency-history
6. 캐시 TTL 전략
# Jitter 적용 - 캐시 스탬피드 방지
# 모든 키가 동시에 만료되면 DB에 부하 폭주
# TTL에 랜덤 값을 더해 만료 시점을 분산
import random
def set_with_jitter(key, value, base_ttl=3600):
jitter = random.randint(0, base_ttl // 10) # 최대 10% 편차
ttl = base_ttl + jitter
redis.setex(key, ttl, json.dumps(value))
Redis는 단순 캐시를 넘어 메시지 큐, 분산 락, 실시간 랭킹, 세션 관리 등 다양한 용도로 활용된다. 각 패턴의 특성을 이해하고 적재적소에 활용하면 시스템의 성능과 안정성을 크게 향상시킬 수 있다.