Memcached, when used for read-through and write-through, acts less like a cache and more like a primary data store.
Let’s see it in action. Imagine a simple e-commerce product lookup. Normally, you’d hit your database for every product query. With read-through, we’ll try Memcached first. If it’s not there, we grab it from the database, then put it in Memcached for next time.
import memcache
import database_client
mc = memcache.Client(['127.0.0.1:11211'], debug=0)
def get_product(product_id):
# Try Memcached first
product_data = mc.get(f"product:{product_id}")
if product_data:
print(f"Cache hit for product {product_id}")
return product_data
# If not in cache, fetch from database
print(f"Cache miss for product {product_id}. Fetching from DB.")
product_data = database_client.fetch_product_by_id(product_id)
if product_data:
# Store in Memcached for future requests
# Set expiration to 5 minutes (300 seconds)
mc.set(f"product:{product_id}", product_data, time=300)
print(f"Stored product {product_id} in cache.")
return product_data
# Example usage:
product_info = get_product("12345")
print(f"Retrieved: {product_info}")
# Second call will be a cache hit
product_info = get_product("12345")
print(f"Retrieved: {product_info}")
This get_product function embodies the read-through pattern. The application code is responsible for checking Memcached, fetching from the source of truth (the database in this case) if a miss occurs, and then populating Memcached. The key here is that Memcached never has stale data because the application logic ensures it’s only populated after a fresh read from the primary store.
Now, for write-through, the logic is slightly different. When data is updated, we must update both the primary data store and Memcached simultaneously.
import memcache
import database_client
mc = memcache.Client(['127.0.0.1:11211'], debug=0)
def update_product(product_id, new_data):
# Update the primary data store first
try:
database_client.update_product_by_id(product_id, new_data)
print(f"Updated product {product_id} in database.")
# Then, update Memcached
# Use the same key as in read-through
mc.set(f"product:{product_id}", new_data, time=300) # Assuming same expiration
print(f"Updated product {product_id} in cache.")
return True
except Exception as e:
print(f"Error updating product {product_id}: {e}")
# In a real-world scenario, you'd need robust error handling.
# If DB update succeeds but Memcached fails, the cache will be stale.
# If DB update fails, Memcached update shouldn't happen or should be rolled back.
return False
# Example usage:
new_product_details = {"name": "Super Widget", "price": 29.99, "stock": 150}
update_product("12345", new_product_details)
In the write-through pattern, the application explicitly writes to both the cache and the primary data store. This guarantees that Memcached is always consistent with the database, but at the cost of increased write latency and complexity. The application has to manage the atomicity or consistency between the two writes. If the database write succeeds but the Memcached write fails, the cache will be stale. If the database write fails, the Memcached write should ideally not proceed, or it should be invalidated.
The core problem these patterns solve is the trade-off between data freshness and read performance. Databases are great for consistency and complex queries but can be slow for high-volume, simple lookups. Memcached is lightning fast for key-value retrieval but offers no transactional guarantees and limited querying capabilities.
Read-through lets you "prime the pump" by ensuring that after a read miss, the data is immediately available for subsequent reads, drastically reducing latency for repeated access to the same data. The application code orchestrates this: get from cache, if miss, get from DB, set to cache.
Write-through forces consistency. Every write operation hits both the cache and the database. This is crucial when stale data is unacceptable, even for a short period. The application code orchestrates this: update DB, update cache.
The critical insight most people miss is that neither of these patterns is an automatic feature of Memcached itself. Memcached is a simple, distributed in-memory key-value store. It doesn’t know about your database, your application’s business logic, or what "fresh" means. The read-through and write-through behaviors are entirely implemented within your application code. You are responsible for the logic that queries Memcached, handles cache misses by fetching from your primary store, and ensures that writes are propagated to both Memcached and your primary store. This application-level implementation is what gives Memcached its versatility beyond a simple cache.
The next challenge you’ll face is handling cache invalidation when data changes in the primary store without going through your application’s write-through logic, often called cache poisoning.