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

14 KiB

Redis Caching - Detailed Reference

Detailed code examples cho Redis caching patterns trong GoodGo.

Table of Contents

  1. Connection Setup
  2. Cache Service
  3. Shopping Cart
  4. Rate Limiting
  5. Distributed Locks
  6. Session Management

Connection Setup

Redis Configuration

/// <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

/// <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

/// <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

/// <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

/// <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

/// <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