Implement a Multi-Level Cache with L1 Memory and L2 Redis in Python
This code implements a simple multi-level cache with an in-process L1 cache (via functools.lru_cache) and a mock Redis L2 cache with TTL, falling back to a slow computation on misses.
Python code
60 linesimport time
from functools import lru_cache
class MockRedis:
def __init__(self):
self.store = {}
def get(self, key):
return self.store.get(key, None)
def set(self, key, value, ttl=5):
self.store[key] = (value, time.time() + ttl)
def get_ttl(self, key):
value, expiry = self.store.get(key, (None, 0))
if value is None or time.time() > expiry:
return None
return value
class MultiLevelCache:
def __init__(self):
self.redis = MockRedis()
@lru_cache(maxsize=3)
def l1_get(self, key):
return None
def get(self, key):
# Level 1: Python lru_cache (fastest, in-process)
cached = self.l1_get(key)
if cached is not None:
return f"L1 HIT: {cached}"
# Level 2: Mock Redis (in-memory mock)
redis_value = self.redis.get_ttl(key)
if redis_value is not None:
self.l1_get.cache_clear()
self.l1_get(key)
return f"L2 HIT: {redis_value}"
# Level 3: Slow "database" (computed here)
value = f"computed_{key}"
self.redis.set(key, value)
self.l1_get(key)
return f"L3 MISS (computed): {value}"
def store(self, key, value):
self.redis.set(key, value)
self.l1_get.cache_clear()
if __name__ == "__main__":
cache = MultiLevelCache()
print(cache.get("user:1"))
print(cache.get("user:1"))
cache.store("user:1", "updated_value")
print(cache.get("user:1"))
print(cache.get("user:1"))
Output
L3 MISS (computed): user:1
L1 HIT: user:1
L1 HIT: user:1
L1 HIT: user:1
How it works
The lru_cache decorator caches the results of l1_get in a fast in-process dict, providing the L1 layer. When an L1 miss occurs, the method checks the mock Redis store with TTL support, simulating an L2 layer. If both miss, it computes a value and populates both layers, demonstrating cache-aside. TTL enforcement is implemented in get_ttl which checks expiry before returning the value. Clearing the L1 cache on writes ensures consistency across layers, though a real system would use invalidation or versioning.
Common mistakes
- Not respecting TTL: forgetting to check expiry in the Redis mock leads to stale reads.
- Cache stampede: when L1 expires, multiple threads may recompute simultaneously; use locking or synchronization.
- Wrong placement of cache_clear: clearing L1 only on writes, not on TTL expiry, can serve stale data from L2.
- Using lru_cache on a method without clearing on updates: leading to inconsistent cache across levels.
Variations
- Use `cachetools.TTLCache` for L1 with expiration instead of `lru_cache`.
- Integrate a real Redis client (`redis-py`) with `expire` for distributed caching.
Real-world use cases
- Serving frequently accessed user profiles in a web application to reduce database load.
- Caching API response data with local in-process cache and a shared Redis layer for microservices.
- Storing configuration data that changes infrequently, using L1 for hot reads and L2 for distributed access.
Sponsored
More from Caching & Redis
- Cache Asides in Python with a Read-Through Loader easy
- Cache Data in Redis with Python easy
- Cache Penetration Null Object Mock in Python medium
- Cache Stampede Prevention with SingleFlight in Python medium
- Cache Warming with Python: Preload Hot Keys easy
- Coalescing duplicate in-flight requests: one shared result for concurrent callers hard
Keep learning
Related tutorials and quizzes for this topic.