07-redis.md

Module 07: Redis

Goal: Master Redis data structures, caching patterns, pub/sub, rate limiting, and queues. Time: 2-3 days of focused study Prerequisites: Module 01-04


Table of Contents

  1. What is Redis?
  2. Redis Setup & CLI
  3. Data Structures
  4. Node.js Redis Client
  5. Caching Patterns
  6. Session Storage
  7. Pub/Sub — Real-Time Messaging
  8. Rate Limiting with Redis
  9. Queues & Background Jobs
  10. Redis Streams
  11. Distributed Locking
  12. Leaderboards & Rankings
  13. Redis Best Practices
  14. Practice Problems
  15. Interview Questions

1. What is Redis?

Redis (Remote Dictionary Server) is an in-memory data store that can be used as:

  • Cache — Speed up database queries
  • Database — Primary store for certain data
  • Message broker — Pub/Sub for real-time messaging
  • Queue — Background job processing
  • Session store — User session management

Why Redis Is Fast

Traditional Database: Redis: Disk → Memory → Process → Return Memory → Process → Return HDD: ~10ms per read RAM: ~0.1ms per read SSD: ~0.1ms per read (100x faster than SSD!) Redis can handle 100,000+ operations per second on a single instance.

Key Characteristics

✅ In-memory (extremely fast) ✅ Single-threaded (no locking issues, atomic operations) ✅ Persistence options (snapshots, append-only file) ✅ Rich data structures (not just key-value!) TTL (time-to-live) support on any key ✅ Pub/Sub messaging ✅ Lua scripting (atomic multi-step operations) ✅ Clustering and replication

2. Redis Setup & CLI

# Install Redis # macOS: brew install redis # Ubuntu: sudo apt install redis-server # Windows: Use Docker or WSL # Docker: docker run -d -p 6379:6379 redis # Start Redis server redis-server # Connect with CLI redis-cli # Basic commands redis-cli ping # PONG (server is alive) redis-cli SET name "Alice" redis-cli GET name # "Alice"

Essential CLI Commands

# Key-value basics SET key value # Set a key GET key # Get value DEL key # Delete key EXISTS key # 1 if exists, 0 if not TYPE key # Type of value stored # Expiration SET session:123 "data" EX 3600 # Expires in 3600 seconds EXPIRE key 60 # Set expiry on existing key TTL key # Time remaining (-1 = no expiry, -2 = expired/deleted) PERSIST key # Remove expiry # Key patterns KEYS user:* # Find keys matching pattern (⚠️ SLOW, avoid in production!) SCAN 0 MATCH user:* COUNT 100 # Iterator-based alternative (production-safe) # Database management SELECT 0 # Switch to database 0 (default, 0-15 available) FLUSHDB # Delete all keys in current database FLUSHALL # Delete all keys in ALL databases DBSIZE # Number of keys INFO # Server information

3. Data Structures

Redis has 6 main data structures. Understanding when to use each is crucial.

1. Strings — The Simplest

# Store any value: string, number, JSON, binary (max 512MB) SET user:1:name "Alice" GET user:1:name # "Alice" # Numeric operations (string that looks like a number) SET counter 10 INCR counter # 11 (atomic increment) INCRBY counter 5 # 16 DECR counter # 15 DECRBY counter 3 # 12 INCRBYFLOAT counter 0.5 # 12.5 # Multiple key operations MSET key1 "val1" key2 "val2" key3 "val3" MGET key1 key2 key3 # ["val1", "val2", "val3"] # Set only if not exists (useful for locking) SETNX lock:resource "owner1" # 1 (set, key didn't exist) SETNX lock:resource "owner2" # 0 (not set, key already exists) # Set with expiry in one command SET session:abc "data" EX 3600 NX # Set with 1-hour expiry, only if not exists # Append APPEND greeting "Hello" # "Hello" APPEND greeting " World" # "Hello World"

Use cases: Caching, counters, rate limiting, simple values, session IDs

2. Lists — Ordered Sequences

# Lists are doubly-linked lists — fast insert/remove at both ends LPUSH queue "task1" # Push to LEFT (head) LPUSH queue "task2" # [task2, task1] RPUSH queue "task3" # [task2, task1, task3] LPOP queue # "task2" (remove from left) RPOP queue # "task3" (remove from right) # Blocking pop (waits until data is available — great for queues!) BLPOP queue 30 # Wait up to 30 seconds for an element LRANGE queue 0 -1 # Get all elements LRANGE queue 0 9 # Get first 10 elements LLEN queue # Length LINDEX queue 0 # Element at index 0 LTRIM queue 0 99 # Keep only first 100 elements (cap list size)

Use cases: Message queues, activity feeds, recent items, task queues

3. Sets — Unique Unordered Collections

SADD tags:post:1 "javascript" "nodejs" "redis" SADD tags:post:2 "javascript" "react" "css" SMEMBERS tags:post:1 # {"javascript", "nodejs", "redis"} SISMEMBER tags:post:1 "redis" # 1 (true) SCARD tags:post:1 # 3 (cardinality/size) SREM tags:post:1 "redis" # Remove element # Set operations SINTER tags:post:1 tags:post:2 # {"javascript"} (intersection) SUNION tags:post:1 tags:post:2 # {"javascript", "nodejs", "redis", "react", "css"} SDIFF tags:post:1 tags:post:2 # {"nodejs", "redis"} (in 1 but not in 2) SRANDMEMBER tags:post:1 2 # Get 2 random members SPOP tags:post:1 # Remove and return random member

Use cases: Tags, unique visitors, mutual friends, online users, voting

4. Sorted Sets — Sets with Scores

# Each member has a score — sorted by score automatically ZADD leaderboard 100 "alice" ZADD leaderboard 250 "bob" ZADD leaderboard 175 "charlie" ZADD leaderboard 300 "diana" # Get by rank (0-indexed, ascending) ZRANGE leaderboard 0 -1 # ["alice", "charlie", "bob", "diana"] ZRANGE leaderboard 0 -1 WITHSCORES # With scores # Get by rank (descending — top players first) ZREVRANGE leaderboard 0 2 # ["diana", "bob", "charlie"] (top 3) ZREVRANGE leaderboard 0 2 WITHSCORES # Get rank ZRANK leaderboard "bob" # 2 (0-indexed, ascending) ZREVRANK leaderboard "bob" # 1 (0-indexed, descending) # Get score ZSCORE leaderboard "bob" # 250 # Increment score ZINCRBY leaderboard 50 "alice" # alice now has 150 # Count and range by score ZCOUNT leaderboard 100 200 # Members with score 100-200 ZRANGEBYSCORE leaderboard 100 200 # Get those members # Remove ZREM leaderboard "alice" ZREMRANGEBYRANK leaderboard 0 0 # Remove lowest-ranked member

Use cases: Leaderboards, priority queues, time-series data, delayed tasks

5. Hashes — Objects/Maps

# Like a mini key-value store inside a key — perfect for objects HSET user:1 name "Alice" age "25" email "alice@example.com" role "admin" HGET user:1 name # "Alice" HGETALL user:1 # {name: "Alice", age: "25", ...} HMGET user:1 name email # ["Alice", "alice@example.com"] HSET user:1 age 26 # Update a field HDEL user:1 email # Delete a field HEXISTS user:1 name # 1 (true) HLEN user:1 # Number of fields HKEYS user:1 # All field names HVALS user:1 # All values HINCRBY user:1 age 1 # Increment numeric field HINCRBYFLOAT user:1 balance 10.50

Use cases: User profiles, configuration, any object-like data, counters per entity

6. Streams — Append-Only Log

# Like a persistent, consumer-group-aware message queue XADD mystream * sensor "temp" value "22.5" # Returns: "1705312000000-0" (timestamp-based ID) XADD mystream * sensor "temp" value "23.1" XADD mystream * sensor "humidity" value "65" # Read messages XRANGE mystream - + # All messages XRANGE mystream - + COUNT 10 # First 10 messages XLEN mystream # Number of messages # Consumer groups (for distributed processing) XGROUP CREATE mystream mygroup 0 # Create group starting from beginning XREADGROUP GROUP mygroup consumer1 COUNT 1 BLOCK 5000 STREAMS mystream > # Read 1 new message, wait up to 5 seconds XACK mystream mygroup "1705312000000-0" # Acknowledge message processed

Use cases: Event sourcing, activity logging, message queues with delivery guarantees


4. Node.js Redis Client

// npm install redis const { createClient } = require('redis'); // ---- CONNECTION ---- const client = createClient({ url: 'redis://localhost:6379', // Or with password: // url: 'redis://:password@localhost:6379' socket: { reconnectStrategy: (retries) => { if (retries > 10) return new Error('Max reconnect attempts reached'); return Math.min(retries * 100, 3000); // Exponential backoff }, }, }); client.on('error', (err) => console.error('Redis error:', err)); client.on('connect', () => console.log('Redis connected')); client.on('reconnecting', () => console.log('Redis reconnecting...')); await client.connect(); // ---- STRINGS ---- await client.set('key', 'value'); await client.set('key', 'value', { EX: 3600 }); // With expiry const value = await client.get('key'); await client.incr('counter'); await client.incrBy('counter', 5); // ---- HASHES ---- await client.hSet('user:1', { name: 'Alice', email: 'alice@example.com', age: '25', }); const user = await client.hGetAll('user:1'); // { name: 'Alice', email: 'alice@example.com', age: '25' } const name = await client.hGet('user:1', 'name'); await client.hIncrBy('user:1', 'loginCount', 1); // ---- LISTS ---- await client.lPush('queue', 'task1'); await client.rPush('queue', 'task2'); const task = await client.lPop('queue'); const items = await client.lRange('queue', 0, -1); // ---- SETS ---- await client.sAdd('online', 'user1', 'user2', 'user3'); const isOnline = await client.sIsMember('online', 'user1'); const onlineUsers = await client.sMembers('online'); // ---- SORTED SETS ---- await client.zAdd('leaderboard', [ { score: 100, value: 'alice' }, { score: 250, value: 'bob' }, { score: 175, value: 'charlie' }, ]); const top3 = await client.zRangeWithScores('leaderboard', 0, 2, { REV: true }); // [{ value: 'bob', score: 250 }, { value: 'charlie', score: 175 }, ...] const rank = await client.zRevRank('leaderboard', 'bob'); // 0 (top) // ---- EXPIRY ---- await client.expire('key', 60); // Expire in 60 seconds const ttl = await client.ttl('key'); // Time remaining // ---- TRANSACTIONS (MULTI/EXEC) ---- // Execute multiple commands atomically const results = await client .multi() .set('key1', 'val1') .set('key2', 'val2') .incr('counter') .exec(); // All commands execute together or none do // ---- CLEANUP ---- await client.quit(); // Graceful close // or await client.disconnect(); // Force close

5. Caching Patterns

Cache-Aside (Lazy Loading) — Most Common

// The application manages the cache manually async function getUser(userId) { const cacheKey = `user:${userId}`; // 1. Check cache const cached = await redis.get(cacheKey); if (cached) { console.log('Cache HIT'); return JSON.parse(cached); } // 2. Cache miss — fetch from database console.log('Cache MISS'); const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]); if (!user) return null; // 3. Store in cache for next time await redis.set(cacheKey, JSON.stringify(user), { EX: 3600 }); // 1 hour TTL return user; } // Update: invalidate cache when data changes async function updateUser(userId, data) { await db.query('UPDATE users SET name = $1 WHERE id = $2', [data.name, userId]); await redis.del(`user:${userId}`); // Invalidate cache // Next read will fetch fresh data from DB and re-cache }

Write-Through Cache

// Every write goes to BOTH cache and database async function createUser(data) { const user = await db.query( 'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *', [data.name, data.email] ); // Update cache immediately await redis.set(`user:${user.id}`, JSON.stringify(user), { EX: 3600 }); return user; }

Write-Behind Cache (Write-Back)

// Write to cache immediately, sync to database later (in batches) // Good for: counters, analytics, non-critical data async function incrementPageView(pageId) { // Fast: increment in Redis await redis.incr(`views:${pageId}`); // Database update happens later via background job } // Background job (runs every minute) async function syncViewsToDB() { const keys = await redis.keys('views:*'); // Use SCAN in production for (const key of keys) { const pageId = key.split(':')[1]; const views = await redis.getSet(key, '0'); // Get and reset if (parseInt(views) > 0) { await db.query( 'UPDATE pages SET views = views + $1 WHERE id = $2', [parseInt(views), pageId] ); } } }

Cache Stampede Prevention

// Problem: When a popular cache key expires, 1000 requests hit the DB simultaneously // Solution: Lock to ensure only one request rebuilds the cache async function getWithLock(key, fetchFn, ttl = 3600) { // Try cache first const cached = await redis.get(key); if (cached) return JSON.parse(cached); // Try to acquire lock const lockKey = `lock:${key}`; const acquired = await redis.set(lockKey, '1', { EX: 10, NX: true }); // 10-second lock if (acquired) { try { // This request rebuilds the cache const data = await fetchFn(); await redis.set(key, JSON.stringify(data), { EX: ttl }); return data; } finally { await redis.del(lockKey); // Release lock } } else { // Another request is rebuilding — wait and retry await new Promise(resolve => setTimeout(resolve, 100)); return getWithLock(key, fetchFn, ttl); // Retry } } // Usage: const user = await getWithLock( `user:${userId}`, () => db.query('SELECT * FROM users WHERE id = $1', [userId]), 3600 );

6. Session Storage

// Using Redis as session store (see Module 05 for full implementation) const session = require('express-session'); const RedisStore = require('connect-redis').default; app.use(session({ store: new RedisStore({ client: redisClient }), secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { secure: true, httpOnly: true, maxAge: 86400000 }, })); // What happens in Redis: // SET "sess:sessionId123" '{"userId":1,"role":"admin"}' EX 86400 // GET "sess:sessionId123" → {"userId":1,"role":"admin"} // DEL "sess:sessionId123" → logout

7. Pub/Sub

Pub/Sub lets you send messages between different parts of your system in real-time.

// ---- PUBLISHER ---- const publisher = createClient({ url: 'redis://localhost:6379' }); await publisher.connect(); // Publish a message to a channel await publisher.publish('notifications', JSON.stringify({ type: 'order_created', orderId: 123, userId: 456, })); await publisher.publish('chat:room:general', JSON.stringify({ from: 'alice', message: 'Hello everyone!', })); // ---- SUBSCRIBER ---- const subscriber = createClient({ url: 'redis://localhost:6379' }); await subscriber.connect(); // Subscribe to channels await subscriber.subscribe('notifications', (message) => { const data = JSON.parse(message); console.log('Notification:', data); }); // Pattern subscribe (wildcard) await subscriber.pSubscribe('chat:room:*', (message, channel) => { const room = channel.split(':')[2]; const data = JSON.parse(message); console.log(`[${room}] ${data.from}: ${data.message}`); }); // ⚠️ IMPORTANT: A subscriber client CANNOT be used for other commands! // You need separate clients for pub and sub.

Real-World: Cross-Server Event Broadcasting

// Use pub/sub to sync events across multiple server instances // event-bus.js class RedisEventBus { constructor(redisUrl) { this.publisher = createClient({ url: redisUrl }); this.subscriber = createClient({ url: redisUrl }); this.handlers = new Map(); } async connect() { await Promise.all([ this.publisher.connect(), this.subscriber.connect(), ]); } async publish(event, data) { await this.publisher.publish(event, JSON.stringify(data)); } async subscribe(event, handler) { if (!this.handlers.has(event)) { this.handlers.set(event, []); await this.subscriber.subscribe(event, (message) => { const data = JSON.parse(message); this.handlers.get(event).forEach(h => h(data)); }); } this.handlers.get(event).push(handler); } } // Usage across your application: const eventBus = new RedisEventBus(process.env.REDIS_URL); await eventBus.connect(); // Server 1: Publish when user signs up eventBus.publish('user:registered', { userId: 1, email: 'alice@test.com' }); // Server 2: Listen and send welcome email eventBus.subscribe('user:registered', async (data) => { await sendWelcomeEmail(data.email); }); // Server 3: Listen and create analytics event eventBus.subscribe('user:registered', async (data) => { await analytics.track('signup', { userId: data.userId }); });

8. Rate Limiting with Redis

Fixed Window

async function rateLimit(userId, limit = 100, windowSecs = 60) { const key = `ratelimit:${userId}:${Math.floor(Date.now() / 1000 / windowSecs)}`; const current = await redis.incr(key); if (current === 1) { await redis.expire(key, windowSecs); } return { allowed: current <= limit, remaining: Math.max(0, limit - current), resetAt: Math.ceil(Date.now() / 1000 / windowSecs) * windowSecs, }; }

Sliding Window (More Accurate)

async function slidingWindowRateLimit(userId, limit = 100, windowMs = 60000) { const key = `ratelimit:${userId}`; const now = Date.now(); const windowStart = now - windowMs; // Use sorted set: score = timestamp, value = unique ID const pipeline = redis.multi(); // Remove old entries outside the window pipeline.zRemRangeByScore(key, 0, windowStart); // Count remaining entries pipeline.zCard(key); // Add current request pipeline.zAdd(key, { score: now, value: `${now}-${Math.random()}` }); // Set expiry on the key pipeline.expire(key, Math.ceil(windowMs / 1000)); const results = await pipeline.exec(); const count = results[1]; // zCard result return { allowed: count < limit, remaining: Math.max(0, limit - count - 1), }; }

Token Bucket (Most Flexible)

async function tokenBucket(userId, maxTokens = 10, refillRate = 1, refillIntervalMs = 1000) { const key = `bucket:${userId}`; // Lua script for atomicity const script = ` local key = KEYS[1] local max_tokens = tonumber(ARGV[1]) local refill_rate = tonumber(ARGV[2]) local refill_interval = tonumber(ARGV[3]) local now = tonumber(ARGV[4]) local bucket = redis.call('HMGET', key, 'tokens', 'last_refill') local tokens = tonumber(bucket[1]) or max_tokens local last_refill = tonumber(bucket[2]) or now -- Calculate tokens to add local elapsed = now - last_refill local refills = math.floor(elapsed / refill_interval) tokens = math.min(max_tokens, tokens + refills * refill_rate) last_refill = last_refill + refills * refill_interval if tokens >= 1 then tokens = tokens - 1 redis.call('HMSET', key, 'tokens', tokens, 'last_refill', last_refill) redis.call('EXPIRE', key, math.ceil(max_tokens / refill_rate * refill_interval / 1000) + 1) return {1, tokens} else redis.call('HMSET', key, 'tokens', tokens, 'last_refill', last_refill) return {0, tokens} end `; const result = await redis.eval(script, { keys: [key], arguments: [ maxTokens.toString(), refillRate.toString(), refillIntervalMs.toString(), Date.now().toString(), ], }); return { allowed: result[0] === 1, remaining: result[1], }; }

9. Queues & Background Jobs

Simple Queue with Lists

// Producer — add jobs to queue async function addJob(queueName, jobData) { const job = { id: crypto.randomUUID(), data: jobData, createdAt: Date.now(), }; await redis.rPush(`queue:${queueName}`, JSON.stringify(job)); return job.id; } // Worker — process jobs from queue async function processQueue(queueName, handler) { console.log(`Worker listening on queue: ${queueName}`); while (true) { // BLPOP blocks until a job is available (timeout: 30s) const result = await redis.blPop(`queue:${queueName}`, 30); if (!result) continue; // Timeout — try again const job = JSON.parse(result.element); console.log(`Processing job: ${job.id}`); try { await handler(job.data); console.log(`Job ${job.id} completed`); } catch (err) { console.error(`Job ${job.id} failed:`, err); // Move to dead letter queue await redis.rPush(`queue:${queueName}:failed`, JSON.stringify({ ...job, error: err.message, failedAt: Date.now(), })); } } } // Usage: // Producer (in your API route): await addJob('email', { to: 'alice@example.com', subject: 'Welcome!' }); // Worker (separate process): processQueue('email', async (data) => { await sendEmail(data.to, data.subject); });

Using BullMQ (Production-Ready Queue)

// npm install bullmq const { Queue, Worker } = require('bullmq'); const connection = { host: 'localhost', port: 6379 }; // ---- PRODUCER ---- const emailQueue = new Queue('email', { connection }); // Add a job await emailQueue.add('welcome', { to: 'alice@example.com', subject: 'Welcome!', template: 'welcome', }, { attempts: 3, // Retry 3 times on failure backoff: { type: 'exponential', delay: 1000, // 1s, 2s, 4s between retries }, removeOnComplete: 100, // Keep last 100 completed jobs removeOnFail: 1000, // Keep last 1000 failed jobs priority: 1, // Lower number = higher priority delay: 60000, // Delay execution by 1 minute }); // Scheduled/repeating jobs await emailQueue.add('daily-digest', {}, { repeat: { pattern: '0 9 * * *', // Every day at 9 AM (cron) }, }); // ---- WORKER (separate process) ---- const worker = new Worker('email', async (job) => { console.log(`Processing job ${job.name} (${job.id})`); switch (job.name) { case 'welcome': await sendWelcomeEmail(job.data.to); break; case 'daily-digest': await sendDailyDigest(); break; } // Return value is stored as job result return { sent: true, timestamp: Date.now() }; }, { connection, concurrency: 5, // Process 5 jobs simultaneously limiter: { max: 10, // Max 10 jobs duration: 1000, // Per second }, }); worker.on('completed', (job, result) => { console.log(`Job ${job.id} completed:`, result); }); worker.on('failed', (job, err) => { console.error(`Job ${job.id} failed:`, err.message); });

10. Redis Streams

Streams are like a persistent, consumer-group-aware message queue.

// ---- PRODUCER ---- // Add events to a stream await redis.xAdd('events', '*', { type: 'page_view', userId: '123', page: '/dashboard', timestamp: Date.now().toString(), }); // ---- CONSUMER (Simple) ---- // Read new events const events = await redis.xRead( { key: 'events', id: '$' }, // $ = only new events { COUNT: 10, BLOCK: 5000 } // Wait up to 5 seconds ); // ---- CONSUMER GROUP (Distributed) ---- // Create consumer group await redis.xGroupCreate('events', 'analytics-group', '0', { MKSTREAM: true }); // Consumer reads from group const messages = await redis.xReadGroup( 'analytics-group', // Group name 'consumer-1', // Consumer name { key: 'events', id: '>' }, // '>' = undelivered messages { COUNT: 10, BLOCK: 5000 } ); // Acknowledge processing if (messages) { for (const [stream, entries] of messages) { for (const { id, message } of entries) { // Process message console.log('Event:', message); // Acknowledge (remove from pending) await redis.xAck('events', 'analytics-group', id); } } }

11. Distributed Locking

// Ensure only one instance processes a task at a time class RedisLock { constructor(client) { this.client = client; } async acquire(resource, ttlMs = 10000) { const lockId = crypto.randomUUID(); const key = `lock:${resource}`; const acquired = await this.client.set(key, lockId, { PX: ttlMs, // Milliseconds NX: true, // Only if not exists }); if (acquired) { return lockId; // Return lock ID for safe release } return null; // Failed to acquire } async release(resource, lockId) { const key = `lock:${resource}`; // Only release if we own the lock (Lua script for atomicity) const script = ` if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end `; return await this.client.eval(script, { keys: [key], arguments: [lockId], }); } async withLock(resource, fn, ttlMs = 10000) { const lockId = await this.acquire(resource, ttlMs); if (!lockId) throw new Error(`Could not acquire lock on ${resource}`); try { return await fn(); } finally { await this.release(resource, lockId); } } } // Usage: const lock = new RedisLock(redis); await lock.withLock('process-payment:order-123', async () => { // Only ONE instance can execute this at a time await processPayment(orderId); });

12. Leaderboards & Rankings

// Perfect use case for Sorted Sets class Leaderboard { constructor(client, name) { this.client = client; this.key = `leaderboard:${name}`; } async addScore(userId, score) { await this.client.zAdd(this.key, { score, value: userId }); } async incrementScore(userId, points) { return await this.client.zIncrBy(this.key, points, userId); } async getTop(count = 10) { const results = await this.client.zRangeWithScores(this.key, 0, count - 1, { REV: true }); return results.map((entry, index) => ({ rank: index + 1, userId: entry.value, score: entry.score, })); } async getRank(userId) { const rank = await this.client.zRevRank(this.key, userId); const score = await this.client.zScore(this.key, userId); return rank !== null ? { rank: rank + 1, score } : null; } async getAroundUser(userId, range = 2) { const rank = await this.client.zRevRank(this.key, userId); if (rank === null) return null; const start = Math.max(0, rank - range); const end = rank + range; const results = await this.client.zRangeWithScores(this.key, start, end, { REV: true }); return results.map((entry, index) => ({ rank: start + index + 1, userId: entry.value, score: entry.score, isCurrentUser: entry.value === userId, })); } } // Usage: const lb = new Leaderboard(redis, 'weekly'); await lb.addScore('alice', 100); await lb.addScore('bob', 250); await lb.incrementScore('alice', 50); console.log(await lb.getTop(10)); console.log(await lb.getRank('alice')); console.log(await lb.getAroundUser('alice'));

13. Redis Best Practices

1. KEY NAMING CONVENTION Use colons as separators: user:123:profile, session:abc123 Use meaningful prefixes: cache:, lock:, queue:, rate: Keep keys short (Redis stores them in memory) 2. ALWAYS SET TTL Every key should have an expiry unless it's permanent data Prevents Redis from running out of memory 3. USE PIPELINES FOR BULK OPERATIONS await client.multi() .set('key1', 'val1') .set('key2', 'val2') .exec(); Reduces round trips (1 instead of N) 4. AVOID KEYS COMMAND IN PRODUCTION KEYS * scans ALL keys — blocks Redis Use SCAN instead (cursor-based, non-blocking) 5. DON'T STORE LARGE VALUES Keep values under 100KB For large data, store in DB and cache metadata in Redis 6. USE THE RIGHT DATA STRUCTURE Need uniqueness? → Set Need ordering? → Sorted Set or List Need fields? → Hash Need simple value? → String 7. HANDLE CONNECTION FAILURES Implement reconnection logic Have fallback behavior when Redis is down 8. MONITOR MEMORY USAGE redis-cli INFO memory Set maxmemory and eviction policy: CONFIG SET maxmemory 256mb CONFIG SET maxmemory-policy allkeys-lru

14. Practice Problems

Problem 1: Build a URL Shortener Cache

// Implement caching for a URL shortener (like Snips!) // Cache short URL → long URL mappings class URLShortenerCache { constructor(redisClient) { this.client = redisClient; } async cacheUrl(shortCode, longUrl, ttl = 86400) { await this.client.set(`url:${shortCode}`, longUrl, { EX: ttl }); } async getUrl(shortCode) { return await this.client.get(`url:${shortCode}`); } async incrementClicks(shortCode) { const key = `clicks:${shortCode}`; const today = new Date().toISOString().split('T')[0]; await this.client.multi() .incr(`${key}:total`) .incr(`${key}:daily:${today}`) .exec(); } async getClickStats(shortCode) { const total = await this.client.get(`clicks:${shortCode}:total`); return { total: parseInt(total) || 0 }; } }

Problem 2: Implement a Session Manager

// Build a session manager using Redis hashes class SessionManager { constructor(client, ttlSeconds = 3600) { this.client = client; this.ttl = ttlSeconds; } async create(userId, metadata = {}) { const sessionId = crypto.randomUUID(); const key = `session:${sessionId}`; await this.client.hSet(key, { userId: userId.toString(), createdAt: Date.now().toString(), ...Object.fromEntries( Object.entries(metadata).map(([k, v]) => [k, String(v)]) ), }); await this.client.expire(key, this.ttl); // Track user's sessions await this.client.sAdd(`user_sessions:${userId}`, sessionId); return sessionId; } async get(sessionId) { const data = await this.client.hGetAll(`session:${sessionId}`); return Object.keys(data).length ? data : null; } async destroy(sessionId) { const data = await this.get(sessionId); if (data) { await this.client.del(`session:${sessionId}`); await this.client.sRem(`user_sessions:${data.userId}`, sessionId); } } async destroyAllForUser(userId) { const sessions = await this.client.sMembers(`user_sessions:${userId}`); if (sessions.length) { const keys = sessions.map(s => `session:${s}`); await this.client.del(keys); await this.client.del(`user_sessions:${userId}`); } } }

15. Interview Questions

Q1: What is Redis? Why is it fast?

Answer: Redis is an in-memory data store that can function as a cache, database, and message broker. It's fast because data is stored in RAM (not disk), it's single-threaded (no locking overhead), and it uses efficient data structures. It can handle 100,000+ operations per second.

Q2: What Redis data structure would you use for a leaderboard?

Answer: Sorted Set (ZSET). Each member has a score, and Redis keeps them sorted automatically. Use ZREVRANGE for top-N players, ZREVRANK for a user's rank, ZINCRBY to update scores. All operations are O(log N). For a weekly leaderboard, use a key like leaderboard:weekly:2024-W03 with TTL.

Q3: How would you implement rate limiting with Redis?

Answer: Multiple approaches: (1) Fixed window — use INCR with EXPIRE, counting requests per time window. (2) Sliding window — use a Sorted Set with timestamps as scores, removing old entries outside the window. (3) Token bucket — use a Hash to track tokens and last refill time. Sliding window is most accurate; token bucket is most flexible.

Q4: Explain Redis Pub/Sub vs Streams.

Answer: Pub/Sub is fire-and-forget — messages go to currently connected subscribers only. If a subscriber is offline, it misses messages. No persistence, no consumer groups. Streams are persistent (like Kafka lite) — messages are stored and can be read by consumer groups with acknowledgment. Use Pub/Sub for real-time events where missing messages is okay. Use Streams when you need delivery guarantees.

Q5: What caching strategy would you use and why?

Answer: Cache-Aside (Lazy Loading) is most common — check cache, miss → query DB → store in cache. Write-Through for data that's read immediately after write. Write-Behind for high-write scenarios like counters. Always set TTL to prevent stale data. Use cache stampede prevention (locking) for popular keys.

Q6: What happens when Redis runs out of memory?

Answer: Depends on the maxmemory-policy: noeviction rejects writes, allkeys-lru removes least recently used keys, volatile-lru removes LRU keys with TTL only, allkeys-random removes random keys. For caches, allkeys-lru is recommended. Always set maxmemory in production.


Next Module: 08 - Testing, Debugging & DevOps — Jest, Docker, CI/CD.