The most surprising thing about Memcached’s cache-aside pattern is that it’s fundamentally a write-through strategy for reads, not a lazy loading one.
Let’s see it in action. Imagine a service that needs to fetch user profile data.
import memcache
import json
mc = memcache.Client(['127.0.0.1:11211'], debug=0)
def get_user_profile(user_id):
# 1. Try to get from cache
cache_key = f"user_profile:{user_id}"
profile_data = mc.get(cache_key)
if profile_data:
print(f"Cache hit for user {user_id}")
return json.loads(profile_data)
else:
print(f"Cache miss for user {user_id}. Fetching from database...")
# 2. If not in cache, fetch from the source of truth (e.g., database)
profile_data = fetch_from_database(user_id) # Assume this function exists
if profile_data:
# 3. Store in cache for future requests
mc.set(cache_key, json.dumps(profile_data), time=3600) # Cache for 1 hour
print(f"Stored user {user_id} in cache.")
return profile_data
def fetch_from_database(user_id):
# Simulate database lookup
print(f"Simulating database lookup for user {user_id}...")
# In a real app, this would be a DB query
return {"user_id": user_id, "name": f"User {user_id}", "email": f"user{user_id}@example.com"}
# First request for user 123
profile = get_user_profile(123)
print(f"Retrieved profile: {profile}")
# Second request for user 123
profile = get_user_profile(123)
print(f"Retrieved profile: {profile}")
When you run this, you’ll see:
Cache miss for user 123. Fetching from database...
Simulating database lookup for user 123...
Stored user 123 in cache.
Retrieved profile: {'user_id': 123, 'name': 'User 123', 'email': 'user123@example.com'}
Cache hit for user 123
Retrieved profile: {'user_id': 123, 'name': 'User 123', 'email': 'user123@example.com'}
The problem this pattern solves is the classic "thundering herd" problem and the performance bottleneck of repeatedly hitting a slow primary data store for frequently accessed data. By placing Memcached in front of the database, we can serve most read requests from memory, which is orders of magnitude faster.
Internally, the cache-aside pattern operates as follows:
- Read Request: The application first checks if the requested data exists in the cache (Memcached).
- Cache Hit: If found, the data is returned directly from the cache. This is the fast path.
- Cache Miss: If the data is not found in the cache, the application then queries the primary data store (e.g., a database).
- Cache Population: Upon retrieving the data from the primary store, the application writes this data into the cache for subsequent requests.
- Return Data: Finally, the data is returned to the application.
The "lazy" part of the name refers to when the data is loaded into the cache – only when it’s first requested and not found. However, the crucial insight is that for every read operation that results in a cache miss, the application performs two operations: a read from the database and a write to Memcached. This is why it behaves more like a "write-through" cache for reads: the cache is always "written" to after the data is retrieved from the source of truth, ensuring it’s populated for the next access.
The key levers you control are the cache_key generation and the cache time (TTL - Time To Live). The cache_key must be unique and consistent for a given piece of data. A common mistake is using a dynamic part of the key that changes unexpectedly, leading to cache misses when data is actually in the cache. The time parameter determines how long an item stays in Memcached before it’s considered stale and removed. Choosing an appropriate TTL is a balance: too short, and you’ll have too many cache misses; too long, and you risk serving stale data if the underlying database record changes.
What most people don’t realize is that Memcached itself doesn’t know or care about your database. It simply stores key-value pairs. The "intelligence" of the cache-aside pattern resides entirely within your application code. Memcached is a dumb, fast, in-memory data store. Your application is responsible for deciding what to cache, when to cache it, and how to handle cache misses and potential data staleness. The application sides with the cache, hence "cache-aside."
The next problem you’ll run into is cache invalidation when the underlying data changes.