Redis — Key-Value at Speed

CS 6500 — Week 10, Session 1

Week 9 Recap + Module Shift

Midterm done — shifting gears

  • Weeks 1–7: Batch processing — move computation to data (HDFS, MapReduce, Spark)
  • Weeks 10–12: Database systems — design storage around access patterns

The core question from here on:

"For this workload — what database is the right tool?"

The NoSQL Landscape

Category Examples Best For
Key-Value Redis, DynamoDB O(1) lookups, sessions, cache
Document MongoDB, CouchDB Flexible schema, rich queries
Wide-Column Cassandra, ScyllaDB Write-heavy, time-series (Week 11)
Graph Neo4j Relationship traversal

This week: Redis (Session 1) + MongoDB (Session 2)

Unifying theme: Choose the right tool for the access pattern, not the data type.

Why Relational Isn't Always the Answer

Problem: 10 million user sessions. Every page load reads one.

Approach Latency Cost
PostgreSQL (disk) 1–10 ms Low — but adds up
Redis (in-memory) < 1 ms Higher RAM, lower CPU

At scale:

  • 100,000 req/sec × 5 ms = 500 CPU-seconds/sec of DB load
  • 100,000 req/sec × 0.2 ms = 20 CPU-seconds/sec

Redis isn't faster because it cheats — it's faster because RAM is faster than disk, and simplicity removes overhead.

What Is Redis?

REmote DIctionary Server — an in-memory data structure server

Key properties:

  • In-memory → sub-millisecond latency (no disk I/O on reads)
  • Single-threaded event loop → no locks, predictable performance
  • Rich data structures → not just key → string; key → any structure
  • Persistence options → RDB snapshots + AOF (append-only file)

Redis is not just a cache — it's a data structure server used as:

  • Session store
  • Leaderboard / sorted set engine
  • Rate limiter
  • Pub/Sub message broker
  • Job queue

Redis Data Structures

Structure Commands Use Case
String SET, GET, INCR, EXPIRE Counters, rate limiting, simple cache
Hash HSET, HGET, HMSET User profiles, session data
List LPUSH, RPOP, LRANGE Job queues, activity feeds
Set SADD, SMEMBERS, SINTERSTORE Unique tags, follower lists
Sorted Set ZADD, ZRANGE, ZRANK Leaderboards, priority queues

Design principle: Pick the structure that matches your query, not your data.

Demo: Redis Strings and Sorted Sets

docker exec -it redis redis-cli
# Strings — session storage with expiry
SET session:user123 '{"name":"Alice","role":"admin"}'
EXPIRE session:user123 3600
TTL session:user123          # Check remaining seconds

# Sorted Sets — leaderboard
ZADD leaderboard 9500 "alice"
ZADD leaderboard 8200 "bob"
ZADD leaderboard 7100 "charlie"

ZREVRANGE leaderboard 0 2 WITHSCORES   # Top 3 (descending)
ZRANK leaderboard "bob"                 # 0-indexed rank
ZINCRBY leaderboard 500 "charlie"       # Update score

Demo: Redis Hashes and Lists

# Hashes — store structured data without serializing to JSON
HSET user:42 name "Bob" age 29 role "analyst"
HGET user:42 name
HMGET user:42 name role        # Multiple fields at once
HGETALL user:42                # Full hash as flat list

# Lists — FIFO job queue
LPUSH job_queue '{"task":"etl","dataset":"sales_2025"}'
LPUSH job_queue '{"task":"report","dataset":"users"}'
LRANGE job_queue 0 -1          # Inspect queue
RPOP job_queue                  # Worker dequeues from right

Key insight: Hashes avoid JSON serialization overhead — Redis stores and retrieves individual fields without deserializing the whole object.

Caching Pattern 1: Cache-Aside (Lazy Loading)

Read path:

1. Application → Redis (GET key)
2. Cache HIT  → return data immediately
3. Cache MISS → read from DB → SET in Redis with TTL → return data

Write path: Application writes to DB only. Cache is populated on next read.

Trade-offs:

  • ✅ Only cache what's actually requested
  • ✅ Cache can tolerate Redis restarts (DB is source of truth)
  • ❌ First read after miss is slower (cache miss penalty)
  • ❌ Cache can serve stale data between DB update and TTL expiry

Best for: Read-heavy workloads, data that can tolerate brief staleness.

Cache-Aside in Python

import redis, json

r = redis.Redis(host='redis', port=6379, decode_responses=True)

def get_user(user_id):
    key = f"user:{user_id}"

    # 1. Try cache first
    cached = r.get(key)
    if cached:
        print(f"Cache HIT for {key}")
        return json.loads(cached)

    # 2. Cache miss — hit the DB
    print(f"Cache MISS for {key}")
    user = db.query(f"SELECT * FROM users WHERE id={user_id}")

    # 3. Populate cache with 5-minute TTL
    r.setex(key, 300, json.dumps(user))
    return user

Caching Pattern 2: Write-Through

Write path:

1. Application writes to DB
2. Application (or ORM) also writes to Redis simultaneously
3. Cache and DB stay in sync after every write

Trade-offs:

  • ✅ Cache is always fresh — no stale reads
  • ✅ Low read latency for recently written data
  • ❌ Write latency increases (two writes per operation)
  • ❌ Cache fills with data that may never be read (write amplification)

Best for: Workloads where stale reads are unacceptable (financial balances, inventory counts).

TTL and Eviction Policies

TTL (Time To Live) — automatic key expiration:

SET session:abc "token"
EXPIRE session:abc 1800     # Expires in 30 minutes
SETEX counter:day 86400 0   # Set + expire in one command
TTL session:abc             # Remaining seconds (-1 = no expiry, -2 = gone)

Eviction policies (when maxmemory is hit):

Policy Behavior
noeviction Reject writes — application sees error
allkeys-lru Evict least-recently-used key across all keys
volatile-lru Evict LRU keys that have a TTL set
allkeys-random Evict random key

Rule of thumb: Use allkeys-lru for caches; noeviction for primary data stores.

Redis Pub/Sub

Pattern: Decouple producers from consumers with message channels

# Terminal 1 — subscriber
SUBSCRIBE notifications:user123

# Terminal 2 — publisher
PUBLISH notifications:user123 '{"type":"alert","msg":"New login"}'

Use cases:

  • Real-time notifications
  • Chat systems
  • Event streaming (lightweight — no persistence, no replay)

Limitation: Pub/Sub has no persistence — if a subscriber is offline, it misses the message. For guaranteed delivery, use Redis Streams (XADD/XREAD).

Activity: Redis Lab (20 min)

Pairs | Docker Redis container

Task 1 — Leaderboard

  • Add 10 players with scores using ZADD
  • Retrieve the top 3 players with scores
  • Update one player's score; verify their new rank

Task 2 — Session Management

  • Store a user session as a Hash (HSET)
  • Set a 30-second TTL; verify with TTL
  • Watch it expire (or DEL it manually to simulate logout)

Task 3 — Rate Limiter (bonus)

  • Use INCR + EXPIRE to build a "max 5 requests per 60 seconds" limiter
  • Key structure: ratelimit:{user_id}:{minute}

Deliverable: Screenshot of redis-cli output + 3 sentences on your design choices

Activity Debrief

Rate limiter solution:

# Check and increment counter
INCR ratelimit:user123:2025012514   # minute-granularity key
# → returns new count

# Set TTL only on first request (count == 1)
EXPIRE ratelimit:user123:2025012514 60
def check_rate_limit(user_id):
    key = f"ratelimit:{user_id}:{int(time.time() // 60)}"
    count = r.incr(key)
    if count == 1:
        r.expire(key, 60)
    return count <= 5  # True = request allowed

Why this works: The key auto-expires after 60 seconds — no cleanup needed.

When Would You NOT Use Redis?

Good discussion — think before answering

Redis is wrong when you need:

  • Datasets larger than RAM — Redis is memory-bound; costs spike
  • Complex relationships — no joins, no foreign keys
  • Ad-hoc analytics — no columnar storage, aggregations are expensive
  • Durability guarantees — AOF helps, but Redis is not a ledger
  • Rich query language — no SQL, no secondary indexes by default

Teaser for Session 2:

"MongoDB gives you flexible schema AND rich queries AND disk persistence — why not use it for everything Redis does?"

Session 1 Key Takeaways

  • Redis = in-memory data structure server, not just a cache
  • 5 core structures: String, Hash, List, Set, Sorted Set — each optimized for a specific access pattern
  • Cache-aside: populate on miss; write-through: populate on write
  • TTL + LRU eviction manage memory automatically
  • Pub/Sub for real-time decoupled messaging

Next session: MongoDB — document model, schema design, aggregation pipelines

Reminder: Quiz 10 opens after Session 2 — due before Week 11.

Speaker context: Students return from midterm and Spring Break. Today launches the NoSQL module — a big mental shift from batch processing to database systems. Redis is the perfect first NoSQL: it's concrete, fast, and students can build intuition in 10 minutes of redis-cli. Lead with the "Redis is not just a cache" misconception — correct it early. The leaderboard demo is always a crowd-pleaser.

Live demo: Type these live. Ask the class to predict what ZRANK returns before you run it. The "ZINCRBY" line is satisfying to run — watch charlie climb the board.

Circulate: Task 3 trips students up — remind them that INCR creates the key if it doesn't exist, and EXPIRE only needs to be called when count == 1 (first request of the window).