Khối xây dựng Trung bình 5 phút đọc

Cache Redis trong ASP.NET Core: khi nào, ở đâu, ra sao

Cách dùng Redis với ASP.NET Core: IMemoryCache vs IDistributedCache, cache-aside vs write-through, chiến lược invalidation, và cách kết nối dùng được ở scale.

Mục lục
  1. Khi nào thêm cache thực sự giúp?
  2. Nên ngân sách những con số nào cho quyết định cache?
  3. Kiến trúc cache-aside tối thiểu trông thế nào?
  4. Cấu hình .NET 10 cho IDistributedCache + Redis là gì?
  5. Invalidate sao cho không hỏng mọi thứ?
  6. Cache giới thiệu failure mode nào?
  7. Khi nào không nên thêm cache?
  8. Đi tiếp đâu từ đây?

Lần đầu một service đọc nhiều hơn database chịu nổi, câu trả lời đúng là cache đọc lại. Lần thứ hai, câu trả lời đúng là invalidate cache mà không làm hỏng mọi thứ. Chương này xử lý cả hai bằng công cụ .NET có sẵn - phần lớn là năm dòng Program.cs cộng một extension method.

Khi nào thêm cache thực sự giúp?

Ba dấu hiệu cache hợp lý.

Tỉ lệ đọc cao - cùng dữ liệu được đọc nhiều gấp 10 lần so với thay đổi. Catalogue sản phẩm, hồ sơ user, feature flag. Cache gộp 10 lần đọc DB thành 1 + 9 lần đọc cache, và Redis nhanh hơn Postgres khoảng 20x cho lookup theo key.

Giá trị tính toán đắt - join ba bảng, kiểm tra quyền, suy luận ML. DB phục vụ được input nhưng phép tính mới đắt. Cache kết quả, không cache input.

Tải biến động với đuôi nóng - 1% item nhận 90% traffic (phân phối Zipfian, hình dạng web điển hình). Cache 1% nóng giữ DB không bùng nổ trong đỉnh.

Dấu hiệu chống cache cũng rõ: tỉ lệ read/write thấp (hit rate sẽ thảm), yêu cầu tươi nghiêm ngặt (chi phí invalidation > chi phí DB), hay workload đã vừa với page cache của DB (Postgres tự cache page nóng vào RAM miễn phí).

Nên ngân sách những con số nào cho quyết định cache?

Thao tác                     Latency       QPS mỗi node
IMemoryCache.Get             ~50 ns        triệu
Redis GET (LAN)              ~0.5 ms       100K-1M
Postgres SELECT theo PK      ~1-3 ms       30K-100K
Postgres SELECT có join      ~5-50 ms      <10K
DynamoDB GetItem             ~5-10 ms      tuỳ RCU

Quy tắc thô: cache PK lookup Postgres đem lại ~5x; cache join mua ~50x; cache giá trị tính toán mua tới 1000x. Lợi lớn hơn biện minh cho phức tạp invalidation cao hơn.

Kiến trúc cache-aside tối thiểu trông thế nào?

Vòng lặp kinh điển, vẽ ra:

flowchart LR
    Client --> App[ASP.NET Core]
    App -->|1. GET key| Cache[(Redis)]
    Cache -->|2a. hit| App
    Cache -->|2b. miss| App
    App -->|3. khi miss<br/>SELECT| DB[(Postgres)]
    DB --> App
    App -->|4. SET key, TTL| Cache
    App --> Client

Mỗi read thử cache trước; khi miss, app query DB và ghi kết quả lại với TTL. Write invalidate (hoặc để TTL hết) các key liên quan. Đây là pattern trong 90% service .NET và scale xa hơn nhiều người tưởng.

Cấu hình .NET 10 cho IDistributedCache + Redis là gì?

Năm dòng Program.cs:

builder.Services.AddStackExchangeRedisCache(opt =>
{
    opt.Configuration = builder.Configuration.GetConnectionString("Redis");
    opt.InstanceName  = "anhtu-dev:";
});

// Sau đó trong service bất kỳ:
public class ProductService(
    IDistributedCache cache,
    AppDbContext db,
    ILogger<ProductService> log)
{
    public async Task<Product?> GetByIdAsync(int id, CancellationToken ct)
    {
        var key = $"product:{id}";
        var cached = await cache.GetStringAsync(key, ct);
        if (cached is not null)
            return JsonSerializer.Deserialize<Product>(cached);

        var product = await db.Products.FindAsync([id], ct);
        if (product is null) return null;

        await cache.SetStringAsync(
            key,
            JsonSerializer.Serialize(product),
            new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15) },
            ct);

        return product;
    }
}

Ba chi tiết. Prefix InstanceName chống va chạm key giữa các service chia sẻ một Redis. AbsoluteExpiration được ưu tiên hơn SlidingExpiration vì dự đoán được. JsonSerializer là tuỳ chọn nhẹ nhất; cho path nóng, MessagePack cắt payload 3-5x.

Invalidate sao cho không hỏng mọi thứ?

Ba chiến lược, tăng dần đúng đắn và chi phí:

Luôn kết hợp với TTL. Message invalidate có thể lạc; TTL là phòng tuyến cuối. Combo này cho bạn deploy mà không mất ngủ.

Cache giới thiệu failure mode nào?

Bốn cái cổ điển:

Chương 13 cho cách lộ cache_hits_total, cache_misses_total, cache_size_bytes qua OpenTelemetry để theo dõi cả bốn.

Khi nào không nên thêm cache?

Khi database gốc phục vụ được tải. Thêm Redis kéo theo deployment mới, failure mode mới, và contract invalidation. Nếu QPS read dưới 1000 và DB latency đã <10 ms, cache không mua gì và tốn vận hành. Với tay vào cache khi toán từ chương 2 cho thấy DB không theo kịp, hoặc khi chi phí join trong chương 5 thành điểm nghẽn.

Đi tiếp đâu từ đây?

Chương kế tiếp: chọn database cho app .NET - cách quyết định giữa SQL và NoSQL, khi nào read replica giúp, và shard thực sự tốn gì. Cache là cái bạn dùng tới trước; chọn database quyết định bạn có bao giờ cần đến không.

Câu hỏi thường gặp

Khi nào chọn IMemoryCache thay Redis?
Khi dữ liệu nhỏ, giống nhau giữa các replica, và chấp nhận warm độc lập từng máy. Ví dụ: feature flag, counter trong process, bảng tra cứu nhỏ. Khi đã deploy nhiều instance, mọi thứ phải nhất quán giữa replica - inventory, state rate-limiter, session - thuộc về Redis sau IDistributedCache.
Cache-aside hay write-through - cái nào mặc định?
Cache-aside, hầu như luôn vậy. Write-through nối write path với cache và tạo failure mode khi Redis trục trặc. Cache-aside chấp nhận read sau write có thể miss cache và trả phí DB; vậy là ổn. Ngoại lệ là khi không thể chịu cold-read latency - lúc đó write-through bù được.
Đặt TTL bao nhiêu?
Đặt TTL dài nhất bạn có thể chấp nhận data cũ, rồi chia đôi. Cho session: phút. Cho catalogue sản phẩm: giờ. Cho 'top user tháng này': một ngày. Kỷ luật là mọi cache entry đều có expiry tuyệt đối - chỉ dựa vào invalidation sẽ leak memory và tạo key ma không ai xoá nổi.
Phát hiện cache stampede ra sao?
Theo dõi DB latency p99 và metric Redis miss-per-second cùng lúc. Stampede thể hiện qua sụt cache hit rate ngắn kèm đỉnh DB connection count và query latency. Sửa bằng single-flight locking - request đầu miss sẽ populate, các request khác chờ - hoặc pre-warm key hot trước khi hết hạn.