# 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 /// /// 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. /// // 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(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 /// /// 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. /// public class RedisCacheService : ICacheService { private readonly IDistributedCache _cache; private readonly IConnectionMultiplexer _redis; private readonly ILogger _logger; private readonly JsonSerializerOptions _jsonOptions; public RedisCacheService( IDistributedCache cache, IConnectionMultiplexer redis, ILogger logger) { _cache = cache; _redis = redis; _logger = logger; _jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; } public async Task GetAsync(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(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( 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 GetOrSetAsync( string key, Func> factory, TimeSpan? expiry = null, CancellationToken ct = default) { var cached = await GetAsync(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(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 /// /// EN: Shopping cart stored in Redis. /// VI: Giỏ hàng lưu trong Redis. /// public class Cart { public string Id { get; set; } = default!; public string UserId { get; set; } = default!; public List 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 /// /// EN: Redis-backed cart repository. /// VI: Repository giỏ hàng với Redis. /// public class RedisCartRepository : ICartRepository { private readonly IDatabase _db; private readonly ILogger _logger; private static readonly TimeSpan DefaultExpiry = TimeSpan.FromDays(7); public RedisCartRepository( IConnectionMultiplexer redis, ILogger logger) { _db = redis.GetDatabase(); _logger = logger; } public async Task GetByUserIdAsync(string userId, CancellationToken ct = default) { var key = GetKey(userId); var data = await _db.StringGetAsync(key); if (data.IsNullOrEmpty) return null; return JsonSerializer.Deserialize(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 /// /// EN: Redis-based sliding window rate limiter. /// VI: Rate limiter sliding window với Redis. /// public class RedisRateLimiter : IRateLimiter { private readonly IDatabase _db; private readonly ILogger _logger; public RedisRateLimiter( IConnectionMultiplexer redis, ILogger logger) { _db = redis.GetDatabase(); _logger = logger; } public async Task 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 /// /// EN: Distributed lock using Redis. /// VI: Distributed lock dùng Redis. /// public class RedisLockService : IDistributedLockService { private readonly IDatabase _db; private readonly ILogger _logger; public async Task 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)