This article explores the common pitfall of cache stampedes when scaling systems, moving beyond a simple cache-aside strategy. It highlights how concurrent requests targeting a cache miss can overwhelm the backend database, leading to performance degradation and potential system failures. The discussion focuses on effective mitigation strategies to ensure system stability under high load.
Read original on Medium #system-designThe basic cache-aside pattern involves checking the cache first, and if a miss occurs, fetching data from the database and then populating the cache. While effective for individual requests, this strategy falls short in distributed, concurrent environments. When multiple clients request the same uncached data simultaneously, they all attempt to fetch it from the backend database, creating a "thundering herd" or cache stampede.
Impact of Cache Stampedes
Cache stampedes can lead to significant performance issues, including database overload, increased latency, resource exhaustion, and potential downtime. This problem is exacerbated by high traffic, cache evictions, or cold starts.
To prevent cache stampedes, several techniques can be employed to serialize or reduce concurrent database access for the same key. These methods aim to ensure only one request, or a limited number, reaches the backend when a cache miss occurs.
import threading
import time
cache = {}
cache_locks = {}
db_data = {"item1": "data1", "item2": "data2"}
def get_data_from_db(key):
time.sleep(0.1) # Simulate DB latency
return db_data.get(key, None)
def get_data(key):
if key in cache:
return cache[key]
# Acquire a lock for this key
if key not in cache_locks:
cache_locks[key] = threading.Lock()
with cache_locks[key]:
# Double-check if data was cached while waiting for lock
if key in cache:
return cache[key]
print(f"Cache miss for {key}. Fetching from DB...")
data = get_data_from_db(key)
if data:
cache[key] = data
return data
# Example usage in a concurrent scenario
# threads = []
# for _ in range(5):
# t = threading.Thread(target=get_data, args=("item1",))
# threads.append(t)
# t.start()
# for t in threads:
# t.join()