Migrate
This commit is contained in:
@@ -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