Episode 3 — NodeJS MongoDB Backend Architecture / 3.16 — Caching with Redis

3.16.b — Introduction to Redis

Redis (Remote Dictionary Server) is an open-source, in-memory data structure store used as a database, cache, message broker, and streaming engine, supporting rich data types like Strings, Lists, Sets, Sorted Sets, and Hashes.


<< Previous: 3.16.a — What is Caching | Next: 3.16.c — Redis with Node.js >>


1. What is Redis?

Redis stands for Remote Dictionary Server. Created by Salvatore Sanfilippo in 2009, it is an in-memory key-value store that has become the de facto standard for caching in modern applications.

Unlike traditional databases that store data on disk, Redis keeps the entire dataset in memory, making reads and writes extraordinarily fast -- typically sub-millisecond.

Traditional Database (Disk-based):
  Write → Disk I/O → Acknowledge        (~5-50ms)
  Read  → Disk I/O → Parse → Return     (~5-50ms)

Redis (Memory-based):
  Write → RAM → Acknowledge             (~0.1-0.5ms)
  Read  → RAM → Return                  (~0.1-0.5ms)

Key Characteristics

CharacteristicDescription
In-memoryAll data lives in RAM for sub-millisecond access
Single-threadedOne thread processes all commands sequentially (no race conditions)
Rich data typesStrings, Hashes, Lists, Sets, Sorted Sets, Streams, and more
PersistenceOptional disk persistence via RDB snapshots or AOF log
Atomic operationsAll commands are atomic; transactions via MULTI/EXEC
Pub/SubBuilt-in publish/subscribe messaging
ReplicationMaster-replica architecture for high availability
ClusteringHorizontal scaling via Redis Cluster
Lua scriptingServer-side scripting for complex operations

2. Installing Redis

macOS (Homebrew)

# Install Redis
brew install redis

# Start Redis server
brew services start redis

# Or start manually (foreground)
redis-server

# Verify it is running
redis-cli ping
# Output: PONG

Ubuntu / Debian

sudo apt update
sudo apt install redis-server

# Start the service
sudo systemctl start redis-server
sudo systemctl enable redis-server

# Verify
redis-cli ping
# Output: PONG

Docker (Recommended for Development)

# Pull and run Redis in Docker
docker run --name my-redis -p 6379:6379 -d redis

# Connect with redis-cli inside the container
docker exec -it my-redis redis-cli

# Stop/start
docker stop my-redis
docker start my-redis

Verify Installation

redis-cli
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> SET hello "world"
OK
127.0.0.1:6379> GET hello
"world"
127.0.0.1:6379> quit

3. Redis Data Types

Redis is far more than a simple key-value store. It supports rich data structures, each with specialized commands.

3.1 Strings

The simplest data type. A String can hold text, numbers, or binary data (up to 512 MB).

# Set and get
SET name "Alice"
GET name                    # "Alice"

# Set with expiration (seconds)
SET session "abc123" EX 3600

# Set with expiration (milliseconds)
SET session "abc123" PX 60000

# Set only if key does NOT exist (useful for locks)
SETNX lock:order "processing"

# Increment/Decrement (atomic counters)
SET visitors 0
INCR visitors               # 1
INCR visitors               # 2
INCRBY visitors 10          # 12
DECR visitors               # 11
DECRBY visitors 5           # 6

# Multiple set/get
MSET first "Alice" last "Smith" age "30"
MGET first last age         # "Alice" "Smith" "30"

# String length
STRLEN name                 # 5

# Append
APPEND name " Johnson"      # "Alice Johnson"

3.2 Lists

Ordered collections of strings. Implemented as linked lists, so pushing/popping from head or tail is O(1).

# Push to left (head) / right (tail)
LPUSH tasks "Task 1"
LPUSH tasks "Task 2"
RPUSH tasks "Task 3"
# List is now: ["Task 2", "Task 1", "Task 3"]

# Get range (0-based index, -1 = last element)
LRANGE tasks 0 -1           # All items
LRANGE tasks 0 1            # First 2 items

# Pop from left / right
LPOP tasks                  # "Task 2"
RPOP tasks                  # "Task 3"

# Get length
LLEN tasks                  # 1

# Get by index
LINDEX tasks 0              # "Task 1"

# Trim (keep only range)
RPUSH logs "log1" "log2" "log3" "log4" "log5"
LTRIM logs 0 2              # Keep only first 3: ["log1", "log2", "log3"]

# Blocking pop (waits for data, great for queues)
BLPOP queue 30              # Wait up to 30 seconds for an item

Use cases: Message queues, recent activity feeds, task queues, chat history.

3.3 Sets

Unordered collections of unique strings. No duplicates allowed.

# Add members
SADD tags "javascript" "nodejs" "redis"
SADD tags "javascript"       # 0 (already exists, not added)

# Get all members
SMEMBERS tags               # {"javascript", "nodejs", "redis"}

# Check membership
SISMEMBER tags "redis"      # 1 (true)
SISMEMBER tags "python"     # 0 (false)

# Count members
SCARD tags                  # 3

# Remove a member
SREM tags "redis"

# Set operations
SADD frontend "html" "css" "javascript" "react"
SADD backend "nodejs" "javascript" "python" "redis"

SINTER frontend backend     # {"javascript"} (intersection)
SUNION frontend backend     # All unique members (union)
SDIFF frontend backend      # {"html", "css", "react"} (in frontend but not backend)

# Random member
SRANDMEMBER tags            # Random element
SPOP tags                   # Remove and return random element

Use cases: Tags, unique visitors tracking, online users, common friends.

3.4 Sorted Sets (ZSets)

Like Sets, but each member has a score used for ordering. Members are unique; scores can repeat.

# Add with scores
ZADD leaderboard 100 "Alice"
ZADD leaderboard 85 "Bob"
ZADD leaderboard 92 "Charlie"
ZADD leaderboard 78 "Diana"

# Get range by rank (ascending)
ZRANGE leaderboard 0 -1                # Diana, Bob, Charlie, Alice
ZRANGE leaderboard 0 -1 WITHSCORES     # With scores

# Get range by rank (descending)
ZREVRANGE leaderboard 0 2              # Top 3: Alice, Charlie, Bob

# Get rank of a member
ZRANK leaderboard "Alice"              # 3 (0-based, ascending)
ZREVRANK leaderboard "Alice"           # 0 (top of leaderboard)

# Get score
ZSCORE leaderboard "Alice"             # 100

# Increment score
ZINCRBY leaderboard 15 "Bob"           # Bob's score is now 100

# Range by score
ZRANGEBYSCORE leaderboard 80 100       # Members with score 80-100
ZCOUNT leaderboard 80 100              # Count in score range

# Remove
ZREM leaderboard "Diana"

# Count
ZCARD leaderboard                       # 3

Use cases: Leaderboards, rankings, priority queues, time-series data, rate limiting.

3.5 Hashes

Maps of field-value pairs. Perfect for representing objects.

# Set fields
HSET user:1001 name "Alice" email "alice@example.com" age "30" role "admin"

# Get a single field
HGET user:1001 name              # "Alice"

# Get multiple fields
HMGET user:1001 name email       # "Alice" "alice@example.com"

# Get all fields and values
HGETALL user:1001                # name, Alice, email, alice@example.com, ...

# Check if field exists
HEXISTS user:1001 name           # 1

# Get all field names
HKEYS user:1001                  # name, email, age, role

# Get all values
HVALS user:1001                  # Alice, alice@example.com, 30, admin

# Increment a numeric field
HINCRBY user:1001 age 1          # 31

# Delete a field
HDEL user:1001 role

# Count fields
HLEN user:1001                   # 3

Use cases: User profiles, product details, session data, configuration objects.


4. Redis Data Types Summary

TypeDescriptionKey CommandsCommon Use Case
StringSingle value (text/number/binary)SET, GET, INCR, MSETCaching, counters, locks
ListOrdered collectionLPUSH, RPUSH, LRANGE, LPOPQueues, feeds, logs
SetUnordered unique collectionSADD, SMEMBERS, SINTERTags, unique tracking
Sorted SetScored unique collectionZADD, ZRANGE, ZREVRANGELeaderboards, rankings
HashField-value mapHSET, HGET, HGETALLObjects, profiles, sessions

5. Redis CLI Basics

Key Management Commands

# Check if key exists
EXISTS user:1001              # 1 (exists) or 0 (not found)

# Delete key(s)
DEL user:1001                 # 1 (deleted) or 0 (not found)
DEL key1 key2 key3            # Delete multiple

# Set expiration (TTL)
EXPIRE key 3600               # Expire in 3600 seconds
PEXPIRE key 60000             # Expire in 60000 milliseconds
EXPIREAT key 1700000000       # Expire at Unix timestamp

# Check remaining TTL
TTL key                       # Seconds remaining (-1 = no expiry, -2 = not found)
PTTL key                      # Milliseconds remaining

# Remove expiration (make persistent)
PERSIST key

# Find keys by pattern (development only!)
KEYS user:*                   # All keys starting with "user:"
KEYS *                        # All keys (NEVER in production!)

# SCAN (production-safe alternative to KEYS)
SCAN 0 MATCH user:* COUNT 10

# Get key type
TYPE user:1001                # hash, string, list, set, zset

# Rename a key
RENAME old-key new-key

# Dump database info
INFO                          # Server info and stats
INFO memory                   # Memory usage details
DBSIZE                        # Number of keys in current database

Important CLI Tips

# Select database (0-15, default is 0)
SELECT 1                      # Switch to database 1

# Flush current database
FLUSHDB                       # Delete all keys in current DB

# Flush ALL databases
FLUSHALL                      # Delete everything (DANGEROUS!)

# Monitor all commands in real time
MONITOR                       # See every command (debugging tool)

# Measure latency
redis-cli --latency
redis-cli --latency-history

6. Persistence: RDB vs AOF

By default, Redis stores everything in memory. If the process crashes, data is lost. Redis offers two persistence mechanisms to save data to disk.

6.1 RDB (Redis Database Snapshots)

Point-in-time snapshots of the dataset at specified intervals.

redis.conf:
  save 900 1          # Snapshot if 1+ key changed in 900 seconds
  save 300 10         # Snapshot if 10+ keys changed in 300 seconds
  save 60 10000       # Snapshot if 10000+ keys changed in 60 seconds
How RDB works:
  1. Redis forks a child process
  2. Child writes entire dataset to a temporary .rdb file
  3. When complete, replaces old .rdb file atomically
  4. Main process continues serving requests during save
ProsCons
Compact binary file, great for backupsData loss between snapshots
Fast restarts (loading .rdb is quick)Fork can be slow for large datasets
Minimal performance impactNot suitable for zero-data-loss requirements

6.2 AOF (Append Only File)

Logs every write operation. Can replay the log to reconstruct the dataset.

redis.conf:
  appendonly yes
  appendfsync always      # Fsync every write (safest, slowest)
  appendfsync everysec    # Fsync every second (good compromise)
  appendfsync no          # Let OS decide when to fsync (fastest, least safe)
How AOF works:
  1. Every write command is appended to the AOF file
  2. On restart, Redis replays all commands to rebuild dataset
  3. Redis can rewrite (compact) the AOF file in background
ProsCons
Minimal data loss (at most 1 second with everysec)Larger file size than RDB
Human-readable log formatSlower restarts (must replay all commands)
Automatic rewriting to compact the fileSlightly more I/O overhead

6.3 Which to Use?

ScenarioRecommendation
Cache only (data loss is acceptable)No persistence, or RDB only
General purposeRDB + AOF together
Maximum durabilityAOF with appendfsync always
Fast backups and disaster recoveryRDB snapshots
DevelopmentDefault settings (RDB)
COMBINED (recommended for production):
  - RDB for fast backups and restarts
  - AOF for durability between snapshots
  - Redis will use AOF to rebuild on restart (more complete)

7. Redis vs Memcached

Both are in-memory caching systems, but they have significant differences.

FeatureRedisMemcached
Data typesStrings, Hashes, Lists, Sets, Sorted Sets, StreamsStrings only
PersistenceRDB + AOFNone (pure cache)
ReplicationBuilt-in master-replicaNone built-in
Pub/SubBuilt-inNot available
TransactionsMULTI/EXECNot available
Lua scriptingSupportedNot available
ClusteringRedis ClusterClient-side sharding
Max value size512 MB1 MB
ThreadingSingle-threaded (I/O threads in 6.0+)Multi-threaded
Memory efficiencySlightly higher overheadMore memory efficient for simple strings
Eviction policies8 policies (LRU, LFU, random, etc.)LRU only

When to Choose Redis

  • You need data structures beyond simple strings
  • You need persistence (data survival across restarts)
  • You need Pub/Sub messaging
  • You need transactions and atomic operations
  • You want a single tool for caching, sessions, queues, and more

When to Choose Memcached

  • You only need simple key-value string caching
  • You want multi-threaded performance for very high throughput
  • Your caching needs are simple and you want a minimal solution
  • You are already running Memcached in your infrastructure

For most Node.js applications, Redis is the clear choice due to its versatility and rich ecosystem.


8. Redis Architecture

Single Server:
  ┌─────────────┐
  │  Application │
  └──────┬──────┘
         │ TCP (port 6379)
  ┌──────▼──────┐
  │    Redis     │
  │   Server     │ ← All data in RAM
  │              │ ← Optional: RDB/AOF on disk
  └──────────────┘

Master-Replica (Read Scaling + High Availability):
  ┌──────────┐     ┌──────────┐     ┌──────────┐
  │  App #1  │     │  App #2  │     │  App #3  │
  └────┬─────┘     └────┬─────┘     └────┬─────┘
       │  writes        │  reads         │  reads
  ┌────▼─────┐     ┌────▼─────┐     ┌────▼─────┐
  │  Master  │────>│ Replica 1│     │ Replica 2│
  └──────────┘     └──────────┘     └──────────┘
       │ replication        ▲ replication
       └────────────────────┘

Redis Cluster (Horizontal Scaling):
  ┌──────────────────────────────────────────┐
  │              Redis Cluster                │
  │  ┌────────┐  ┌────────┐  ┌────────┐     │
  │  │ Node 1 │  │ Node 2 │  │ Node 3 │     │
  │  │ 0-5460 │  │5461-10922│ │10923-16383│  │
  │  └────────┘  └────────┘  └────────┘     │
  │  Data is automatically sharded across    │
  │  nodes by hash slot (16384 slots total)  │
  └──────────────────────────────────────────┘

9. Memory Management and Eviction

When Redis reaches its memory limit, it uses eviction policies to decide what to remove.

redis.conf:
  maxmemory 256mb
  maxmemory-policy allkeys-lru
PolicyDescription
noevictionReturn error on write when memory is full (default)
allkeys-lruRemove least recently used key from all keys
allkeys-lfuRemove least frequently used key from all keys
allkeys-randomRemove a random key from all keys
volatile-lruRemove LRU key among keys with expiration set
volatile-lfuRemove LFU key among keys with expiration set
volatile-randomRemove random key among keys with expiration set
volatile-ttlRemove key with shortest TTL

Recommended for caching: allkeys-lru or allkeys-lfu -- Redis automatically evicts the least useful data when memory is full.


Key Takeaways

  1. Redis is an in-memory data structure store -- not just a key-value cache
  2. It supports five core data types: Strings, Lists, Sets, Sorted Sets, and Hashes
  3. redis-cli provides a REPL for interacting with Redis directly
  4. Redis offers persistence via RDB (snapshots) and AOF (write log) -- use both in production
  5. Redis vs Memcached: Redis wins for most use cases due to richer data types, persistence, and Pub/Sub
  6. Single-threaded design means no race conditions and atomic operations by default
  7. Eviction policies like LRU and LFU manage memory automatically when limits are reached
  8. Master-replica and cluster modes provide high availability and horizontal scaling

Explain-It Challenge

Scenario: You are designing a caching layer for a social media application. You need to store user profiles (name, bio, follower count), a timeline of recent posts (ordered by time), a set of unique hashtags for each post, and a real-time leaderboard of the most active users this week.

For each feature, decide which Redis data type you would use and explain why. Write the Redis CLI commands you would use to create, read, and update the data. How would you handle expiration for each feature? What persistence configuration would you choose?


<< Previous: 3.16.a — What is Caching | Next: 3.16.c — Redis with Node.js >>