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

11 KiB

name, description, compatibility, metadata
name description compatibility metadata
redis-caching Redis caching patterns cho distributed systems. Use for cache-aside, session storage, rate limiting, và distributed locks. .NET 8+, StackExchange.Redis, Microsoft.Extensions.Caching.StackExchangeRedis
author version
Velik Ho 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

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

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

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

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

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

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

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

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

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

# 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