357 lines
11 KiB
Markdown
357 lines
11 KiB
Markdown
---
|
|
name: redis-caching
|
|
description: Redis caching patterns cho distributed systems. Use for cache-aside, session storage, rate limiting, và distributed locks.
|
|
compatibility: ".NET 8+, StackExchange.Redis, Microsoft.Extensions.Caching.StackExchangeRedis"
|
|
metadata:
|
|
author: Velik Ho
|
|
version: "1.0"
|
|
---
|
|
|
|
# Redis Caching Patterns / Mẫu Caching Redis
|
|
|
|
Redis caching và distributed data patterns cho GoodGo microservices.
|
|
|
|
## When to Use This Skill / Khi Nào Sử Dụng
|
|
|
|
Use this skill when:
|
|
- Implementing distributed cache / Triển khai distributed cache
|
|
- Caching API responses / Caching responses API
|
|
- Managing user sessions / Quản lý user sessions
|
|
- Implementing rate limiting / Triển khai rate limiting
|
|
- Using Redis as primary store (e.g., shopping cart) / Dùng Redis làm store chính
|
|
|
|
## Core Concepts / Khái Niệm Cốt Lõi
|
|
|
|
### Caching Strategies / Các Chiến Lược Caching
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ CACHING PATTERNS │
|
|
├─────────────────────┬─────────────────────┬─────────────────┤
|
|
│ CACHE-ASIDE │ WRITE-THROUGH │ WRITE-BEHIND │
|
|
│ (Read pattern) │ (Write pattern) │ (Async write) │
|
|
├─────────────────────┼─────────────────────┼─────────────────┤
|
|
│ 1. Check cache │ 1. Write to cache │ 1. Write cache │
|
|
│ 2. If miss, get DB │ 2. Write to DB sync │ 2. Queue DB │
|
|
│ 3. Populate cache │ │ 3. Async flush │
|
|
└─────────────────────┴─────────────────────┴─────────────────┘
|
|
```
|
|
|
|
### Redis Data Types / Các Kiểu Dữ Liệu Redis
|
|
|
|
| Type | Use Case | Example |
|
|
|------|----------|---------|
|
|
| **String** | Simple cache | User profile, JWT token |
|
|
| **Hash** | Object cache | Shopping cart |
|
|
| **List** | Queues | Task queue |
|
|
| **Set** | Unique items | Active users |
|
|
| **Sorted Set** | Rankings | Leaderboard |
|
|
|
|
### Cache Invalidation / Xóa Cache
|
|
|
|
| Strategy | Description | Use When |
|
|
|----------|-------------|----------|
|
|
| **TTL** | Auto-expire after time | Data changes rarely |
|
|
| **Event-based** | Invalidate on update | Data changes often |
|
|
| **Version key** | Cache with version | Complex objects |
|
|
|
|
## Key Patterns / Mẫu Chính
|
|
|
|
### Redis Configuration
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// EN: Configure Redis distributed cache.
|
|
/// VI: Cấu hình Redis distributed cache.
|
|
/// </summary>
|
|
|
|
// Program.cs
|
|
builder.Services.AddStackExchangeRedisCache(options =>
|
|
{
|
|
options.Configuration = builder.Configuration["Redis:ConnectionString"];
|
|
options.InstanceName = "GoodGo:";
|
|
});
|
|
|
|
// EN: Register Redis connection multiplexer for advanced scenarios
|
|
// VI: Đăng ký Redis connection multiplexer cho các trường hợp nâng cao
|
|
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
|
|
ConnectionMultiplexer.Connect(builder.Configuration["Redis:ConnectionString"]!));
|
|
```
|
|
|
|
### Cache Service Implementation
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// EN: Generic cache service with typed operations.
|
|
/// VI: Cache service generic với typed operations.
|
|
/// </summary>
|
|
public interface ICacheService
|
|
{
|
|
Task<T?> GetAsync<T>(string key, CancellationToken ct = default);
|
|
Task SetAsync<T>(string key, T value, TimeSpan? expiry = null, CancellationToken ct = default);
|
|
Task RemoveAsync(string key, CancellationToken ct = default);
|
|
Task<T> GetOrSetAsync<T>(string key, Func<Task<T>> factory, TimeSpan? expiry = null, CancellationToken ct = default);
|
|
}
|
|
|
|
public class RedisCacheService : ICacheService
|
|
{
|
|
private readonly IDistributedCache _cache;
|
|
private readonly ILogger<RedisCacheService> _logger;
|
|
private static readonly TimeSpan DefaultExpiry = TimeSpan.FromMinutes(30);
|
|
|
|
public RedisCacheService(
|
|
IDistributedCache cache,
|
|
ILogger<RedisCacheService> logger)
|
|
{
|
|
_cache = cache;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task<T?> GetAsync<T>(string key, CancellationToken ct = default)
|
|
{
|
|
var data = await _cache.GetStringAsync(key, ct);
|
|
if (data == null)
|
|
{
|
|
_logger.LogDebug("Cache miss for key: {Key}", key);
|
|
return default;
|
|
}
|
|
|
|
_logger.LogDebug("Cache hit for key: {Key}", key);
|
|
return JsonSerializer.Deserialize<T>(data);
|
|
}
|
|
|
|
public async Task SetAsync<T>(
|
|
string key,
|
|
T value,
|
|
TimeSpan? expiry = null,
|
|
CancellationToken ct = default)
|
|
{
|
|
var options = new DistributedCacheEntryOptions
|
|
{
|
|
AbsoluteExpirationRelativeToNow = expiry ?? DefaultExpiry
|
|
};
|
|
|
|
var data = JsonSerializer.Serialize(value);
|
|
await _cache.SetStringAsync(key, data, options, ct);
|
|
|
|
_logger.LogDebug("Cached key: {Key} with expiry: {Expiry}", key, expiry ?? DefaultExpiry);
|
|
}
|
|
|
|
public async Task RemoveAsync(string key, CancellationToken ct = default)
|
|
{
|
|
await _cache.RemoveAsync(key, ct);
|
|
_logger.LogDebug("Removed cache key: {Key}", key);
|
|
}
|
|
|
|
public async Task<T> GetOrSetAsync<T>(
|
|
string key,
|
|
Func<Task<T>> factory,
|
|
TimeSpan? expiry = null,
|
|
CancellationToken ct = default)
|
|
{
|
|
var cached = await GetAsync<T>(key, ct);
|
|
if (cached != null)
|
|
return cached;
|
|
|
|
var value = await factory();
|
|
await SetAsync(key, value, expiry, ct);
|
|
return value;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Cache-Aside Pattern in Query Handler
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// EN: Query handler with cache-aside pattern.
|
|
/// VI: Query handler với cache-aside pattern.
|
|
/// </summary>
|
|
public class GetUserProfileQueryHandler
|
|
: IRequestHandler<GetUserProfileQuery, UserProfileDto?>
|
|
{
|
|
private readonly ICacheService _cache;
|
|
private readonly IUserRepository _userRepository;
|
|
private static readonly TimeSpan CacheExpiry = TimeSpan.FromMinutes(15);
|
|
|
|
public async Task<UserProfileDto?> Handle(
|
|
GetUserProfileQuery request,
|
|
CancellationToken ct)
|
|
{
|
|
var cacheKey = $"user:profile:{request.UserId}";
|
|
|
|
// EN: Try get from cache first
|
|
// VI: Thử lấy từ cache trước
|
|
return await _cache.GetOrSetAsync(
|
|
cacheKey,
|
|
async () =>
|
|
{
|
|
var user = await _userRepository.GetByIdAsync(request.UserId, ct);
|
|
return user?.ToProfileDto();
|
|
},
|
|
CacheExpiry,
|
|
ct);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Cache Invalidation on Write
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// EN: Invalidate cache when data changes.
|
|
/// VI: Xóa cache khi dữ liệu thay đổi.
|
|
/// </summary>
|
|
public class UpdateUserProfileCommandHandler
|
|
: IRequestHandler<UpdateUserProfileCommand, Unit>
|
|
{
|
|
private readonly ICacheService _cache;
|
|
private readonly IUserRepository _userRepository;
|
|
|
|
public async Task<Unit> Handle(
|
|
UpdateUserProfileCommand request,
|
|
CancellationToken ct)
|
|
{
|
|
var user = await _userRepository.GetByIdAsync(request.UserId, ct)
|
|
?? throw new NotFoundException("User", request.UserId);
|
|
|
|
user.UpdateProfile(request.DisplayName, request.Bio);
|
|
await _userRepository.UnitOfWork.SaveChangesAsync(ct);
|
|
|
|
// EN: Invalidate cache after update
|
|
// VI: Xóa cache sau khi cập nhật
|
|
await _cache.RemoveAsync($"user:profile:{request.UserId}", ct);
|
|
|
|
return Unit.Value;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Shopping Cart with Redis Hash
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// EN: Shopping cart repository using Redis Hash.
|
|
/// VI: Repository giỏ hàng dùng Redis Hash.
|
|
/// </summary>
|
|
public class RedisCartRepository : ICartRepository
|
|
{
|
|
private readonly IDatabase _redis;
|
|
private static readonly TimeSpan CartExpiry = TimeSpan.FromDays(7);
|
|
|
|
public RedisCartRepository(IConnectionMultiplexer redis)
|
|
{
|
|
_redis = redis.GetDatabase();
|
|
}
|
|
|
|
public async Task<Cart?> GetAsync(string cartId)
|
|
{
|
|
var data = await _redis.StringGetAsync($"cart:{cartId}");
|
|
return data.IsNullOrEmpty
|
|
? null
|
|
: JsonSerializer.Deserialize<Cart>(data!);
|
|
}
|
|
|
|
public async Task SaveAsync(Cart cart)
|
|
{
|
|
var key = $"cart:{cart.Id}";
|
|
var data = JsonSerializer.Serialize(cart);
|
|
|
|
await _redis.StringSetAsync(key, data, CartExpiry);
|
|
}
|
|
|
|
public async Task DeleteAsync(string cartId)
|
|
{
|
|
await _redis.KeyDeleteAsync($"cart:{cartId}");
|
|
}
|
|
}
|
|
```
|
|
|
|
## Common Mistakes / Lỗi Thường Gặp
|
|
|
|
### 1. No Cache Expiry
|
|
|
|
```csharp
|
|
// ❌ BAD: No expiry leads to stale data
|
|
await _cache.SetStringAsync(key, data);
|
|
|
|
// ✅ GOOD: Always set expiry
|
|
await _cache.SetStringAsync(key, data, new DistributedCacheEntryOptions
|
|
{
|
|
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30)
|
|
});
|
|
```
|
|
|
|
### 2. Cache Stampede
|
|
|
|
```csharp
|
|
// ❌ BAD: Multiple requests hit DB simultaneously on cache miss
|
|
var data = await _cache.GetAsync<Data>(key);
|
|
if (data == null)
|
|
{
|
|
data = await _db.GetDataAsync();
|
|
await _cache.SetAsync(key, data);
|
|
}
|
|
|
|
// ✅ GOOD: Use locking to prevent stampede
|
|
var data = await _cache.GetOrSetWithLockAsync(key, async () =>
|
|
{
|
|
return await _db.GetDataAsync();
|
|
});
|
|
```
|
|
|
|
### 3. Caching Null Values
|
|
|
|
```csharp
|
|
// ❌ BAD: Not caching null causes repeated DB calls
|
|
if (data != null)
|
|
await _cache.SetAsync(key, data);
|
|
|
|
// ✅ GOOD: Cache null with short TTL
|
|
var cacheValue = new CacheWrapper<Data> { Value = data, IsNull = data == null };
|
|
await _cache.SetAsync(key, cacheValue, data == null ? TimeSpan.FromMinutes(1) : TimeSpan.FromHours(1));
|
|
```
|
|
|
|
## Quick Reference / Tham Chiếu Nhanh
|
|
|
|
### Cache Key Naming
|
|
|
|
```csharp
|
|
// EN: Pattern: {entity}:{id}:{optional-subkey}
|
|
// VI: Pattern: {entity}:{id}:{optional-subkey}
|
|
"user:profile:123"
|
|
"order:items:456"
|
|
"product:details:789"
|
|
"user:orders:123:page:1"
|
|
```
|
|
|
|
### Common TTL Values
|
|
|
|
| Data Type | TTL | Reason |
|
|
|-----------|-----|--------|
|
|
| User profile | 15-30 min | Changes moderately |
|
|
| Product catalog | 1-4 hours | Batch updates |
|
|
| Shopping cart | 7 days | User convenience |
|
|
| Session | 30 min sliding | Security |
|
|
| Rate limit | 1 min | Short window |
|
|
|
|
### Redis Commands via CLI
|
|
|
|
```bash
|
|
# EN: View all keys with pattern
|
|
redis-cli KEYS "user:*"
|
|
|
|
# EN: Get TTL of key
|
|
redis-cli TTL "user:profile:123"
|
|
|
|
# EN: Delete keys by pattern
|
|
redis-cli KEYS "cart:*" | xargs redis-cli DEL
|
|
```
|
|
|
|
## Resources / Tài Nguyên
|
|
|
|
- [Detailed Examples](./references/REFERENCE.md) - Full code examples
|
|
- [Error Handling](../error-handling-patterns/SKILL.md) - Cache failure handling
|
|
- [Repository Pattern](../repository-pattern/SKILL.md) - Data access patterns
|
|
- [Docker Traefik](../docker-traefik/SKILL.md) - Redis container setup
|