Cache Strategies¶
Effective cache strategies balance performance gains with data consistency. This section covers invalidation patterns, cache warming, and optimization techniques for building robust caching systems.
Cache Invalidation Strategies¶
Cache invalidation is one of the hardest problems in computer science. The boilerplate provides several strategies to handle different scenarios while maintaining data consistency.
Understanding Cache Invalidation¶
Cache invalidation ensures that cached data doesn't become stale when the underlying data changes. Poor invalidation leads to users seeing outdated information, while over-aggressive invalidation negates caching benefits.
Basic Invalidation Patterns¶
Time-Based Expiration (TTL)¶
The simplest strategy relies on cache expiration times:
# Set different TTL based on data characteristics
@cache(key_prefix="user_profile", expiration=3600) # 1 hour for profiles
@cache(key_prefix="post_content", expiration=1800) # 30 min for posts
@cache(key_prefix="live_stats", expiration=60) # 1 min for live data
Pros:
- Simple to implement and understand
- Guarantees cache freshness within TTL period
- Works well for data with predictable change patterns
Cons:
- May serve stale data until TTL expires
- Difficult to optimize TTL for all scenarios
- Cache miss storms when many keys expire simultaneously
Write-Through Invalidation¶
Automatically invalidate cache when data is modified:
@router.put("/posts/{post_id}")
@cache(
key_prefix="post_cache",
resource_id_name="post_id",
to_invalidate_extra={
"user_posts": "{user_id}", # User's post list
"category_posts": "{category_id}", # Category post list
"recent_posts": "global" # Global recent posts
}
)
async def update_post(
request: Request,
post_id: int,
post_data: PostUpdate,
user_id: int,
category_id: int
):
# Update triggers automatic cache invalidation
updated_post = await crud_posts.update(db=db, id=post_id, object=post_data)
return updated_post
Pros:
- Immediate consistency when data changes
- No stale data served to users
- Precise control over what gets invalidated
Cons:
- More complex implementation
- Can impact write performance
- Risk of over-invalidation
Advanced Invalidation Patterns¶
Pattern-Based Invalidation¶
Use Redis pattern matching for bulk invalidation:
@router.put("/users/{user_id}/profile")
@cache(
key_prefix="user_profile",
resource_id_name="user_id",
pattern_to_invalidate_extra=[
"user_{user_id}_*", # All user-related caches
"*_user_{user_id}_*", # Caches containing this user
"leaderboard_*", # Leaderboards might change
"search_users_*" # User search results
]
)
async def update_user_profile(request: Request, user_id: int, profile_data: ProfileUpdate):
await crud_users.update(db=db, id=user_id, object=profile_data)
return {"message": "Profile updated"}
Pattern Examples:
# User-specific patterns
"user_{user_id}_posts_*" # All paginated post lists for user
"user_{user_id}_*_cache" # All cached data for user
"*_following_{user_id}" # All caches tracking this user's followers
# Content patterns
"posts_category_{category_id}_*" # All posts in category
"comments_post_{post_id}_*" # All comments for post
"search_*_{query}" # All search results for query
# Time-based patterns
"daily_stats_*" # All daily statistics
"hourly_*" # All hourly data
"temp_*" # Temporary cache entries
Cache Warming Strategies¶
Cache warming proactively loads data into cache to avoid cache misses during peak usage.
Application Startup Warming¶
# core/startup.py
async def warm_critical_caches():
"""Warm up critical caches during application startup."""
logger.info("Starting cache warming...")
# Warm up reference data
await warm_reference_data()
# Warm up popular content
await warm_popular_content()
# Warm up user session data for active users
await warm_active_user_data()
logger.info("Cache warming completed")
async def warm_reference_data():
"""Warm up reference data that rarely changes."""
# Countries, currencies, timezones, etc.
reference_data = await crud_reference.get_all_countries()
for country in reference_data:
cache_key = f"country:{country['code']}"
await cache.client.set(cache_key, json.dumps(country), ex=86400) # 24 hours
# Categories
categories = await crud_categories.get_all()
await cache.client.set("all_categories", json.dumps(categories), ex=3600)
async def warm_popular_content():
"""Warm up frequently accessed content."""
# Most viewed posts
popular_posts = await crud_posts.get_popular(limit=100)
for post in popular_posts:
cache_key = f"post_cache:{post['id']}"
await cache.client.set(cache_key, json.dumps(post), ex=1800)
# Trending topics
trending = await crud_posts.get_trending_topics(limit=50)
await cache.client.set("trending_topics", json.dumps(trending), ex=600)
async def warm_active_user_data():
"""Warm up data for recently active users."""
# Get users active in last 24 hours
active_users = await crud_users.get_recently_active(hours=24)
for user in active_users:
# Warm user profile
profile_key = f"user_profile:{user['id']}"
await cache.client.set(profile_key, json.dumps(user), ex=3600)
# Warm user's recent posts
user_posts = await crud_posts.get_user_posts(user['id'], limit=10)
posts_key = f"user_{user['id']}_posts:page_1"
await cache.client.set(posts_key, json.dumps(user_posts), ex=1800)
# Add to startup events
@app.on_event("startup")
async def startup_event():
await create_redis_cache_pool()
await warm_critical_caches()
These cache strategies provide a comprehensive approach to building performant, consistent caching systems that scale with your application's needs while maintaining data integrity.