Files
pos-system/microservices/.agent/skills/redis-caching/references/REFERENCE.md
Ho Ngoc Hai 76d75c753b Migrate
2026-05-23 18:37:02 +07:00

480 lines
14 KiB
Markdown

# 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)