Migrate
This commit is contained in:
356
microservices/.agent/skills/redis-caching/SKILL.md
Normal file
356
microservices/.agent/skills/redis-caching/SKILL.md
Normal file
@@ -0,0 +1,356 @@
|
||||
---
|
||||
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
|
||||
@@ -0,0 +1,479 @@
|
||||
# Redis Caching - Detailed Reference
|
||||
|
||||
Detailed code examples cho Redis caching patterns trong GoodGo.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Connection Setup](#connection-setup)
|
||||
2. [Cache Service](#cache-service)
|
||||
3. [Shopping Cart](#shopping-cart)
|
||||
4. [Rate Limiting](#rate-limiting)
|
||||
5. [Distributed Locks](#distributed-locks)
|
||||
6. [Session Management](#session-management)
|
||||
|
||||
---
|
||||
|
||||
## Connection Setup
|
||||
|
||||
### Redis Configuration
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// EN: Configure Redis for distributed cache and direct access.
|
||||
/// VI: Cấu hình Redis cho distributed cache và truy cập trực tiếp.
|
||||
/// </summary>
|
||||
|
||||
// appsettings.json
|
||||
{
|
||||
"Redis": {
|
||||
"ConnectionString": "localhost:6379,abortConnect=false,connectTimeout=5000",
|
||||
"InstanceName": "GoodGo:",
|
||||
"DefaultDatabase": 0
|
||||
}
|
||||
}
|
||||
|
||||
// Program.cs
|
||||
builder.Services.AddStackExchangeRedisCache(options =>
|
||||
{
|
||||
options.Configuration = builder.Configuration["Redis:ConnectionString"];
|
||||
options.InstanceName = builder.Configuration["Redis:InstanceName"];
|
||||
});
|
||||
|
||||
// EN: For advanced scenarios (Lua scripts, pub/sub, etc.)
|
||||
// VI: Cho các trường hợp nâng cao (Lua scripts, pub/sub, v.v.)
|
||||
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
|
||||
{
|
||||
var config = ConfigurationOptions.Parse(
|
||||
builder.Configuration["Redis:ConnectionString"]!);
|
||||
config.AbortOnConnectFail = false;
|
||||
config.ConnectRetry = 3;
|
||||
return ConnectionMultiplexer.Connect(config);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cache Service
|
||||
|
||||
### Complete Cache Service Implementation
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// EN: Full-featured cache service with serialization and error handling.
|
||||
/// VI: Cache service đầy đủ tính năng với serialization và xử lý lỗi.
|
||||
/// </summary>
|
||||
public class RedisCacheService : ICacheService
|
||||
{
|
||||
private readonly IDistributedCache _cache;
|
||||
private readonly IConnectionMultiplexer _redis;
|
||||
private readonly ILogger<RedisCacheService> _logger;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
|
||||
public RedisCacheService(
|
||||
IDistributedCache cache,
|
||||
IConnectionMultiplexer redis,
|
||||
ILogger<RedisCacheService> logger)
|
||||
{
|
||||
_cache = cache;
|
||||
_redis = redis;
|
||||
_logger = logger;
|
||||
_jsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<T?> GetAsync<T>(string key, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var data = await _cache.GetStringAsync(key, ct);
|
||||
if (string.IsNullOrEmpty(data))
|
||||
{
|
||||
_logger.LogDebug("Cache MISS: {Key}", key);
|
||||
return default;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Cache HIT: {Key}", key);
|
||||
return JsonSerializer.Deserialize<T>(data, _jsonOptions);
|
||||
}
|
||||
catch (RedisConnectionException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Redis connection failed for key: {Key}", key);
|
||||
return default; // EN: Graceful degradation / VI: Xử lý graceful
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SetAsync<T>(
|
||||
string key,
|
||||
T value,
|
||||
TimeSpan? absoluteExpiry = null,
|
||||
TimeSpan? slidingExpiry = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var options = new DistributedCacheEntryOptions();
|
||||
|
||||
if (absoluteExpiry.HasValue)
|
||||
options.AbsoluteExpirationRelativeToNow = absoluteExpiry;
|
||||
if (slidingExpiry.HasValue)
|
||||
options.SlidingExpiration = slidingExpiry;
|
||||
|
||||
var data = JsonSerializer.Serialize(value, _jsonOptions);
|
||||
await _cache.SetStringAsync(key, data, options, ct);
|
||||
|
||||
_logger.LogDebug("Cache SET: {Key}", key);
|
||||
}
|
||||
catch (RedisConnectionException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to cache key: {Key}", key);
|
||||
// EN: Don't throw - cache is not critical / VI: Không throw - cache không critical
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<T> GetOrSetAsync<T>(
|
||||
string key,
|
||||
Func<CancellationToken, Task<T>> factory,
|
||||
TimeSpan? expiry = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var cached = await GetAsync<T>(key, ct);
|
||||
if (cached != null)
|
||||
return cached;
|
||||
|
||||
// EN: Use lock to prevent cache stampede
|
||||
// VI: Dùng lock để tránh cache stampede
|
||||
var lockKey = $"lock:{key}";
|
||||
var db = _redis.GetDatabase();
|
||||
|
||||
if (await db.LockTakeAsync(lockKey, Environment.MachineName, TimeSpan.FromSeconds(10)))
|
||||
{
|
||||
try
|
||||
{
|
||||
// EN: Double-check after acquiring lock
|
||||
// VI: Kiểm tra lại sau khi lấy lock
|
||||
cached = await GetAsync<T>(key, ct);
|
||||
if (cached != null)
|
||||
return cached;
|
||||
|
||||
var value = await factory(ct);
|
||||
await SetAsync(key, value, expiry, ct: ct);
|
||||
return value;
|
||||
}
|
||||
finally
|
||||
{
|
||||
await db.LockReleaseAsync(lockKey, Environment.MachineName);
|
||||
}
|
||||
}
|
||||
|
||||
// EN: Lock not acquired, wait and retry
|
||||
// VI: Không lấy được lock, chờ và thử lại
|
||||
await Task.Delay(100, ct);
|
||||
return await GetOrSetAsync(key, factory, expiry, ct);
|
||||
}
|
||||
|
||||
public async Task RemoveAsync(string key, CancellationToken ct = default)
|
||||
{
|
||||
await _cache.RemoveAsync(key, ct);
|
||||
_logger.LogDebug("Cache REMOVE: {Key}", key);
|
||||
}
|
||||
|
||||
public async Task RemoveByPatternAsync(string pattern, CancellationToken ct = default)
|
||||
{
|
||||
var server = _redis.GetServer(_redis.GetEndPoints().First());
|
||||
var keys = server.Keys(pattern: pattern).ToArray();
|
||||
|
||||
if (keys.Length > 0)
|
||||
{
|
||||
var db = _redis.GetDatabase();
|
||||
await db.KeyDeleteAsync(keys);
|
||||
_logger.LogDebug("Cache REMOVE pattern: {Pattern}, Count: {Count}", pattern, keys.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Shopping Cart
|
||||
|
||||
### Cart Entity
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// EN: Shopping cart stored in Redis.
|
||||
/// VI: Giỏ hàng lưu trong Redis.
|
||||
/// </summary>
|
||||
public class Cart
|
||||
{
|
||||
public string Id { get; set; } = default!;
|
||||
public string UserId { get; set; } = default!;
|
||||
public List<CartItem> Items { get; set; } = new();
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public decimal TotalAmount => Items.Sum(i => i.Quantity * i.UnitPrice);
|
||||
|
||||
public void AddItem(Guid productId, string productName, int quantity, decimal unitPrice)
|
||||
{
|
||||
var existing = Items.FirstOrDefault(i => i.ProductId == productId);
|
||||
if (existing != null)
|
||||
{
|
||||
existing.Quantity += quantity;
|
||||
existing.UnitPrice = unitPrice; // EN: Update price
|
||||
}
|
||||
else
|
||||
{
|
||||
Items.Add(new CartItem
|
||||
{
|
||||
ProductId = productId,
|
||||
ProductName = productName,
|
||||
Quantity = quantity,
|
||||
UnitPrice = unitPrice
|
||||
});
|
||||
}
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void RemoveItem(Guid productId)
|
||||
{
|
||||
Items.RemoveAll(i => i.ProductId == productId);
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
public class CartItem
|
||||
{
|
||||
public Guid ProductId { get; set; }
|
||||
public string ProductName { get; set; } = default!;
|
||||
public int Quantity { get; set; }
|
||||
public decimal UnitPrice { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### Cart Repository
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// EN: Redis-backed cart repository.
|
||||
/// VI: Repository giỏ hàng với Redis.
|
||||
/// </summary>
|
||||
public class RedisCartRepository : ICartRepository
|
||||
{
|
||||
private readonly IDatabase _db;
|
||||
private readonly ILogger<RedisCartRepository> _logger;
|
||||
private static readonly TimeSpan DefaultExpiry = TimeSpan.FromDays(7);
|
||||
|
||||
public RedisCartRepository(
|
||||
IConnectionMultiplexer redis,
|
||||
ILogger<RedisCartRepository> logger)
|
||||
{
|
||||
_db = redis.GetDatabase();
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<Cart?> GetByUserIdAsync(string userId, CancellationToken ct = default)
|
||||
{
|
||||
var key = GetKey(userId);
|
||||
var data = await _db.StringGetAsync(key);
|
||||
|
||||
if (data.IsNullOrEmpty)
|
||||
return null;
|
||||
|
||||
return JsonSerializer.Deserialize<Cart>(data!);
|
||||
}
|
||||
|
||||
public async Task SaveAsync(Cart cart, CancellationToken ct = default)
|
||||
{
|
||||
var key = GetKey(cart.UserId);
|
||||
var data = JsonSerializer.Serialize(cart);
|
||||
|
||||
await _db.StringSetAsync(key, data, DefaultExpiry);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Cart saved for user {UserId}, Items: {ItemCount}",
|
||||
cart.UserId, cart.Items.Count);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string userId, CancellationToken ct = default)
|
||||
{
|
||||
var key = GetKey(userId);
|
||||
await _db.KeyDeleteAsync(key);
|
||||
|
||||
_logger.LogInformation("Cart deleted for user {UserId}", userId);
|
||||
}
|
||||
|
||||
private static string GetKey(string userId) => $"cart:{userId}";
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
### Redis Rate Limiter
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// EN: Redis-based sliding window rate limiter.
|
||||
/// VI: Rate limiter sliding window với Redis.
|
||||
/// </summary>
|
||||
public class RedisRateLimiter : IRateLimiter
|
||||
{
|
||||
private readonly IDatabase _db;
|
||||
private readonly ILogger<RedisRateLimiter> _logger;
|
||||
|
||||
public RedisRateLimiter(
|
||||
IConnectionMultiplexer redis,
|
||||
ILogger<RedisRateLimiter> logger)
|
||||
{
|
||||
_db = redis.GetDatabase();
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<RateLimitResult> CheckAsync(
|
||||
string key,
|
||||
int maxRequests,
|
||||
TimeSpan window)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
var windowStart = now - (long)window.TotalMilliseconds;
|
||||
var redisKey = $"ratelimit:{key}";
|
||||
|
||||
// EN: Lua script for atomic sliding window
|
||||
// VI: Lua script cho sliding window atomic
|
||||
var script = @"
|
||||
redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[1])
|
||||
local count = redis.call('ZCARD', KEYS[1])
|
||||
if count < tonumber(ARGV[2]) then
|
||||
redis.call('ZADD', KEYS[1], ARGV[3], ARGV[3])
|
||||
redis.call('EXPIRE', KEYS[1], ARGV[4])
|
||||
return {1, count + 1, tonumber(ARGV[2])}
|
||||
else
|
||||
local oldest = redis.call('ZRANGE', KEYS[1], 0, 0, 'WITHSCORES')
|
||||
local retryAfter = oldest[2] + tonumber(ARGV[5]) - tonumber(ARGV[3])
|
||||
return {0, count, tonumber(ARGV[2]), retryAfter}
|
||||
end
|
||||
";
|
||||
|
||||
var result = await _db.ScriptEvaluateAsync(script,
|
||||
new RedisKey[] { redisKey },
|
||||
new RedisValue[]
|
||||
{
|
||||
windowStart,
|
||||
maxRequests,
|
||||
now,
|
||||
(int)window.TotalSeconds,
|
||||
(long)window.TotalMilliseconds
|
||||
});
|
||||
|
||||
var values = (RedisResult[])result!;
|
||||
var allowed = (int)values[0] == 1;
|
||||
var currentCount = (int)values[1];
|
||||
var limit = (int)values[2];
|
||||
var retryAfter = values.Length > 3 ? TimeSpan.FromMilliseconds((long)values[3]) : TimeSpan.Zero;
|
||||
|
||||
if (!allowed)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Rate limit exceeded for {Key}: {Count}/{Limit}",
|
||||
key, currentCount, limit);
|
||||
}
|
||||
|
||||
return new RateLimitResult(allowed, currentCount, limit, retryAfter);
|
||||
}
|
||||
}
|
||||
|
||||
public record RateLimitResult(
|
||||
bool IsAllowed,
|
||||
int CurrentCount,
|
||||
int Limit,
|
||||
TimeSpan RetryAfter);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Distributed Locks
|
||||
|
||||
### Redis Lock Service
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// EN: Distributed lock using Redis.
|
||||
/// VI: Distributed lock dùng Redis.
|
||||
/// </summary>
|
||||
public class RedisLockService : IDistributedLockService
|
||||
{
|
||||
private readonly IDatabase _db;
|
||||
private readonly ILogger<RedisLockService> _logger;
|
||||
|
||||
public async Task<IAsyncDisposable?> AcquireAsync(
|
||||
string resource,
|
||||
TimeSpan expiry,
|
||||
TimeSpan? waitTime = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var lockKey = $"lock:{resource}";
|
||||
var lockValue = Guid.NewGuid().ToString();
|
||||
var deadline = DateTime.UtcNow + (waitTime ?? TimeSpan.Zero);
|
||||
|
||||
do
|
||||
{
|
||||
if (await _db.LockTakeAsync(lockKey, lockValue, expiry))
|
||||
{
|
||||
_logger.LogDebug("Lock acquired: {Resource}", resource);
|
||||
return new RedisLock(_db, lockKey, lockValue, _logger);
|
||||
}
|
||||
|
||||
if (waitTime.HasValue)
|
||||
await Task.Delay(50, ct);
|
||||
|
||||
} while (waitTime.HasValue && DateTime.UtcNow < deadline && !ct.IsCancellationRequested);
|
||||
|
||||
_logger.LogWarning("Failed to acquire lock: {Resource}", resource);
|
||||
return null;
|
||||
}
|
||||
|
||||
private class RedisLock : IAsyncDisposable
|
||||
{
|
||||
private readonly IDatabase _db;
|
||||
private readonly string _key;
|
||||
private readonly string _value;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public RedisLock(IDatabase db, string key, string value, ILogger logger)
|
||||
{
|
||||
_db = db;
|
||||
_key = key;
|
||||
_value = value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _db.LockReleaseAsync(_key, _value);
|
||||
_logger.LogDebug("Lock released: {Key}", _key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EN: Usage / VI: Cách dùng
|
||||
await using var lockHandle = await _lockService.AcquireAsync(
|
||||
$"order:{orderId}",
|
||||
TimeSpan.FromMinutes(1),
|
||||
waitTime: TimeSpan.FromSeconds(5));
|
||||
|
||||
if (lockHandle == null)
|
||||
throw new ConflictException("Order is being processed");
|
||||
|
||||
await ProcessOrderAsync(orderId);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Resources / Tài Nguyên
|
||||
|
||||
- [StackExchange.Redis](https://stackexchange.github.io/StackExchange.Redis/)
|
||||
- [Microsoft Distributed Caching](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed)
|
||||
- [Redis Documentation](https://redis.io/documentation)
|
||||
Reference in New Issue
Block a user