Redis Caching Strategies — Complete Guide
이 글의 핵심
Five Redis caching patterns—Cache-Aside, Read-through, Write-through, Write-behind, Refresh-ahead—with TTL, invalidation, and Node.js-oriented examples for API latency and DB load.
Introduction
Redis is an in-memory key–value store used for sessions, rate limiting, and pub/sub—but caching remains one of its top use cases. How you attach the cache changes consistency, latency, and failure behavior—that is what Cache-Aside, Read-through, Write-through, Write-behind, and Refresh-ahead formalize. This article walks each pattern concept → minimal Node.js sketch → TTL and invalidation. Pair it with running Redis via Docker Compose.
After reading this post
- You can separate responsibilities (app vs cache vs DB) across patterns
- You can explain TTL, cache stampede, and invalidation in operational terms
- You know where to hook logic in Node.js code
Table of contents
- Concepts: why patterns differ
- Pattern behavior and Node.js sketches
- TTL, invalidation, and stampedes
- Advanced: production considerations
- Performance comparison
- Real-world cases
- Troubleshooting
- Conclusion
Concepts: why patterns differ
Basics
- Cache hit: Data is in Redis → skip the database.
- Cache miss: Not in Redis → read source (often DB) and decide whether to populate cache.
- Consistency: How much stale cache after a DB update you tolerate drives pattern choice.
Why it matters
The database is authoritative but expensive; Redis is fast but volatile and size-limited. Patterns define who fills and who invalidates the cache.
Pattern behavior and Node.js sketches
Below assumes ioredis and async functions—minimal examples.
// lib/redis.js
import Redis from 'ioredis';
export const redis = new Redis(process.env.REDIS_URL);
1. Cache-Aside (lazy loading)
The app checks the cache first; on miss, loads from the DB and stores in Redis.
// 실행 예제
async function getUser(id) {
const key = `user:${id}`;
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
const row = await db.query('SELECT * FROM users WHERE id = $1', [id]);
if (!row) return null;
await redis.set(key, JSON.stringify(row), 'EX', 300); // TTL 300s
return row;
}
Traits: Simple and ubiquitous. On writes, the app owns invalidation or refresh rules.
2. Read-through
The cache layer (or store) calls the loader on miss. The app only talks to the cache API.
// Concept sketch: wrapper runs loader on miss
// 실행 예제
async function readThrough(key, ttlSec, loader) {
const hit = await redis.get(key);
if (hit) return JSON.parse(hit);
const value = await loader();
if (value != null)
await redis.set(key, JSON.stringify(value), 'EX', ttlSec);
return value;
}
Traits: Centralizes logic—often easier to maintain when ORM/cache modules own the loader.
3. Write-through
On writes, update both DB and cache for higher consistency.
async function updateUser(id, data) {
await db.query('UPDATE users SET ... WHERE id = $1', [id]);
const row = await db.query('SELECT * FROM users WHERE id = $1', [id]);
await redis.set(`user:${id}`, JSON.stringify(row[0]), 'EX', 300);
}
Traits: Read path stays simple; write latency may increase due to double writes.
4. Write-behind (write-back)
Write to cache first; flush to DB asynchronously in batches. Traits: Can improve write throughput/latency but adds loss risk and complex recovery—often unsuitable for payments, inventory, etc.
5. Refresh-ahead
Background refresh before expiry to reduce miss rate on hot keys. Traits: Helps traffic spikes; requires job design and duplicate-refresh control.
TTL, invalidation, and stampedes
TTL (time to live)
- TTL on keys lets stale data expire naturally.
- Too short → DB load; too long → longer inconsistency windows.
Invalidate on write (pairs well with Cache-Aside)
async function updateUser(id, data) {
await db.query('UPDATE users SET ... WHERE id = $1', [id]);
await redis.del(`user:${id}`);
}
Cache stampede
When a hot key expires, many requests can hit the DB at once. Mitigate with probabilistic early expiration or singleflight (one request loads while others wait or retry).
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function getWithSingleFlight(key, ttl, loader) {
const lockKey = `lock:${key}`;
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
const got = await redis.set(lockKey, '1', 'EX', 10, 'NX');
if (!got) {
await sleep(50);
return getWithSingleFlight(key, ttl, loader);
}
try {
const value = await loader();
await redis.set(key, JSON.stringify(value), 'EX', ttl);
return value;
} finally {
await redis.del(lockKey);
}
}
Advanced: production considerations
- Connection pools: Size Redis clients and DB pools for process count.
- Key naming: Use
service:entity:idstyle to avoid collisions. - Compression: Consider MessagePack or compression for large payloads.
- Failure policy: Document whether you fall back to DB or fail requests when Redis is down.
Performance comparison
| Pattern | Read latency | Write latency | Consistency | Complexity |
|---|---|---|---|---|
| Cache-Aside | Low on hit | Low | Depends on TTL + invalidation | Low |
| Read-through | Low | — | Depends on loader | Medium |
| Write-through | Low | Higher | Relatively stronger | Medium |
| Write-behind | Low | Very low | Hard to guarantee | High |
| Refresh-ahead | Very low on hit | — | Depends on TTL/preload timing | High |
Real-world cases
- Product detail: Cache-Aside + short TTL + invalidate on price change.
- Announcement banner: Read-through wrapper to keep app code small.
- Feeds/rankings: If recomputation is expensive, avoid naive Write-behind—often prefer batch precomputation. See also the Node.js performance guide.
Troubleshooting
| Symptom | Cause | Mitigation |
|---|---|---|
| Stale data intermittently | Missing invalidation | del/version keys on all update paths |
| Redis memory spikes | Large values / unbounded keys | TTL, maxmemory-policy, data design |
| Sudden DB load | Stampede | Singleflight, early refresh |
| Cascading timeouts | Redis slowness | Connection limits, slow logs, network isolation |
Conclusion
- Many teams start with Cache-Aside and evolve toward Write-through or Refresh-ahead when needed.
- TTL plus explicit invalidation keeps operations manageable.
- For database choice, continue with PostgreSQL vs MySQL and database integration.
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. Five Redis caching patterns—Cache-Aside, Read-through, Write-through, Write-behind, Refresh-ahead—with TTL.
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
- Docker Compose로 Node API·PostgreSQL·Redis 한 번에 띄우기
- Node.js 성능 최적화 | 클러스터링, 캐싱, 프로파일링
- PostgreSQL vs MySQL 차이와 선택 가이드 | 스키마·트랜잭션·운영
Keywords Covered in This Article (Related Search Terms)
This article covers Node.js, Redis, Caching, Cache-Aside, Write-through, Performance, Patterns.