Files
TBank-Backend/docs/user-guide/caching/cache-strategies.md
2025-10-19 22:09:35 +03:00

191 lines
6.2 KiB
Markdown

# 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:
```python
# 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:
```python
@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:
```python
@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:**
```python
# 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
```python
# 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.