Redis Data Structures, Caching Patterns & Atomic Rate Limiting
High-scale Python web applications rely on Redis as an in-memory key-value data store, caching engine, and distributed lock manager. Understanding Redis data structures (Strings, Hashes, Sorted Sets ZSET), caching strategies (Cache-Aside, Write-Through), Cache Stampede Mitigation (Probabilistic Early Expiration / Mutexes), and Atomic Token Bucket Rate Limiting using Lua scripts is critical for system architecture interviews.
This chapter details Redis internal data structures, Cache-Aside pattern, Cache Stampede solutions, and atomic Token Bucket rate limiting in Python.
1. Redis Core Data Structures & Use Cases
| Data Structure | Internal Implementation | O(1) / O(N) | Key Production Use Cases |
|---|---|---|---|
| String | Simple Dynamic String (sds) | $O(1)$ | Session tokens, raw JSON string caching, atomic counters (INCR). |
| Hash | ZipList / Dict | $O(1)$ | User profile objects (HSET user:100 name "Alice" age 30). |
| List | QuickList (Linked List of ZipLists) | $O(1)$ head/tail | Background worker job queues (LPUSH / BRPOP). |
| Set | IntSet / HashTable | $O(1)$ | Unique IP tracking, tag matching (SADD, SINTER). |
Sorted Set (ZSET) | SkipList + HashTable | $O(\log N)$ | Leaderboards, Sliding Window Rate Limiters (ZADD). |
2. Enterprise Caching Strategies
1. CACHE-ASIDE (Lazy Loading - Most Popular):
- Read Path: App checks Redis -> If HIT, return. If MISS, query DB -> Store in Redis -> Return.
- Write Path: App updates DB directly -> Invalidates (deletes) cache key in Redis.
2. WRITE-THROUGH:
- App writes to Cache -> Cache synchronously writes to DB before returning success.
3. WRITE-BEHIND (Write-Back):
- App writes to Cache -> Cache asynchronously flushes writes to DB in background batches.3. The Cache Stampede Problem & Solutions
A Cache Stampede (or Thundering Herd) occurs when a hot cache key expires under high traffic (e.g. 10,000 requests/sec). All 10,000 concurrent workers experience a cache miss simultaneously, overwhelming the primary SQL database with duplicate queries!
Cache Stampede Execution Spike:
[ Hot Cache Key Expires! ]
|
βββ Worker 1 Miss ββ> [ Query Primary DB ] βββ
βββ Worker 2 Miss ββ> [ Query Primary DB ] βββΌββ> [ DATABASE CRASHES! ]
βββ Worker N Miss ββ> [ Query Primary DB ] βββSolutions:
- Mutex Locking (Distributed Lock): The first worker acquiring
SET key:lock uuid NX PX 5000queries the DB and populates the cache; other workers wait or return stale data. - Probabilistic Early Expiration (XFetch): As the TTL approaches expiration, requests probabilistically re-compute the cache early based on read frequency and computation time:
Recompute If: -beta * delta * ln(rand()) > TTL
4. Atomic Rate Limiting in Python with Redis Lua Scripts
Rate limiting protects APIs from abuse. Implementing Token Bucket or Sliding Window rate limiters requires Atomicity so concurrent requests donβt bypass limits due to race conditions.
Redis executes Lua scripts atomically within a single thread:
# Atomic Token Bucket Rate Limiter via Redis Lua Script
import redis
redis_client = redis.Redis.from_url("redis://localhost:6379/0")
# Lua Script: Atomically checks and decrements token bucket
TOKEN_BUCKET_LUA = """
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local ttl = tonumber(ARGV[2])
local current = tonumber(redis.call('get', key) or "0")
if current + 1 > limit then
return 0 -- Limit Exceeded!
else
redis.call('INCRBY', key, 1)
if current == 0 then
redis.call('EXPIRE', key, ttl)
end
return 1 -- Request Allowed!
end
"""
rate_limit_script = redis_client.register_script(TOKEN_BUCKET_LUA)
def is_request_allowed(user_id: str, limit: int = 100, window_seconds: int = 60) -> bool:
key = f"rate_limit:{user_id}"
# Executes Lua script atomically on Redis server!
allowed = rate_limit_script(keys=[key], args=[limit, window_seconds])
return bool(allowed)