Tổng quan
Caching là kỹ thuật lưu trữ tạm thời dữ liệu ở tầng nhanh hơn để giảm latency và giảm tải cho data source gốc. Đúng strategy giúp giảm 10-100x response time; sai strategy gây stale data và bug khó debug.
Các Caching Patterns
Cache-Aside (Lazy Loading)
Application tự quản lý cache: đọc cache trước, nếu miss thì đọc DB rồi ghi vào cache.
csharppublic class ProductService
{
private readonly IDistributedCache _cache;
private readonly ProductDbContext _db;
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10);
public async Task<Product?> GetProductAsync(int id, CancellationToken ct = default)
{
var cacheKey = $"product:{id}";
// 1. Thử đọc từ cache
var cached = await _cache.GetStringAsync(cacheKey, ct);
if (cached is not null)
return JsonSerializer.Deserialize<Product>(cached);
// 2. Cache miss - đọc từ DB
var product = await _db.Products.FindAsync(new object[] { id }, ct);
if (product is null)
return null;
// 3. Ghi vào cache
await _cache.SetStringAsync(cacheKey,
JsonSerializer.Serialize(product),
new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = CacheDuration },
ct);
return product;
}
}
Ưu điểm: Chỉ cache data thực sự được request, đơn giản. Nhược điểm: Cache miss đầu tiên luôn chậm, có thể bị stale data trong TTL window.
Write-Through
Mọi write đều ghi vào cả cache và DB đồng thời. Đảm bảo cache luôn consistent.
csharppublic async Task UpdateProductAsync(Product product, CancellationToken ct = default)
{
// 1. Ghi DB trước (source of truth)
_db.Products.Update(product);
await _db.SaveChangesAsync(ct);
// 2. Cập nhật cache ngay lập tức
var cacheKey = $"product:{product.Id}";
await _cache.SetStringAsync(cacheKey,
JsonSerializer.Serialize(product),
new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10) },
ct);
}
Ưu điểm: Cache luôn fresh, read performance tốt. Nhược điểm: Write latency tăng (phải ghi 2 nơi), cache có thể chứa data không bao giờ được đọc.
Write-Behind (Write-Back)
Ghi vào cache trước, batch ghi vào DB sau (async). Phù hợp write-heavy workload.
csharp// Sử dụng Channel để buffer writes
public class WriteBehindProductService
{
private readonly Channel<Product> _writeChannel =
Channel.CreateBounded<Product>(new BoundedChannelOptions(1000)
{
FullMode = BoundedChannelFullMode.Wait
});
public async Task UpdateProductAsync(Product product, CancellationToken ct)
{
// 1. Ghi cache ngay (fast path)
var cacheKey = $"product:{product.Id}";
await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(product), ct);
// 2. Enqueue để ghi DB sau
await _writeChannel.Writer.WriteAsync(product, ct);
}
// Background service batch ghi DB
public async Task ProcessWritesAsync(CancellationToken ct)
{
var batch = new List<Product>();
await foreach (var product in _writeChannel.Reader.ReadAllAsync(ct))
{
batch.Add(product);
if (batch.Count >= 50 || !_writeChannel.Reader.TryPeek(out _))
{
await BulkUpdateDbAsync(batch, ct);
batch.Clear();
}
}
}
}
Ưu điểm: Write rất nhanh, giảm tải DB nhờ batching. Nhược điểm: Rủi ro mất data nếu cache crash trước khi flush, eventually consistent.
So sánh Cache Technologies
| Tiêu chí | In-Memory (MemoryCache) | Redis | CDN |
|---|---|---|---|
| Latency | < 1ms (cùng process) | 1-5ms (network hop) | Tùy geo (0.5-50ms) |
| Capacity | Giới hạn bởi RAM server | Lên đến 120GB/node | Không giới hạn |
| Shared giữa instances | Không | Có | Có |
| Persistence | Không (mất khi restart) | Có (AOF/RDB) | Có |
| Data types | Object reference | String, Hash, List, Set, Sorted Set | Static content |
| Chi phí | Free (dùng RAM app) | ~$200-1000/tháng (Azure) | ~$0.01-0.10/GB |
| Phù hợp | Single instance, hot data | Distributed, session, pub/sub | Static files, API response |
Azure Cache for Redis Setup
bash# Tạo Redis instance (Premium tier cho production)
az redis create \
--resource-group myRG \
--name myapp-redis \
--location southeastasia \
--sku Premium \
--vm-size p1 \
--enable-non-ssl-port false \
--minimum-tls-version 1.2
# Lấy connection string
az redis show \
--resource-group myRG \
--name myapp-redis \
--query "{Host:hostName, Port:sslPort}" -o table
az redis list-keys \
--resource-group myRG \
--name myapp-redis
Đăng ký trong ASP.NET Core
csharp// Program.cs
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration["Redis:ConnectionString"];
options.InstanceName = "myapp:";
});
// Hoặc dùng trực tiếp ConnectionMultiplexer cho advanced scenarios
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
{
var config = ConfigurationOptions.Parse(
builder.Configuration["Redis:ConnectionString"]);
config.AbortOnConnectFail = false;
config.ConnectRetry = 3;
config.SyncTimeout = 3000;
return ConnectionMultiplexer.Connect(config);
});
Cache Invalidation Strategies
"There are only two hard things in Computer Science: cache invalidation and naming things." - Phil Karlton
1. TTL-Based (Time-To-Live)
Đơn giản nhất. Set expiry time, cache tự expire.
csharpawait _cache.SetStringAsync(key, value,
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10), // Hard deadline
SlidingExpiration = TimeSpan.FromMinutes(2) // Reset nếu có access
});
2. Event-Based Invalidation
Invalidate cache khi data thay đổi thông qua events.
csharppublic class ProductUpdatedHandler : INotificationHandler<ProductUpdatedEvent>
{
private readonly IDistributedCache _cache;
public async Task Handle(ProductUpdatedEvent notification, CancellationToken ct)
{
// Xóa cache cụ thể
await _cache.RemoveAsync($"product:{notification.ProductId}", ct);
// Xóa cache liên quan (listing, category...)
await _cache.RemoveAsync($"products:category:{notification.CategoryId}", ct);
await _cache.RemoveAsync("products:featured", ct);
}
}
3. Version-Based (Cache Busting)
Thêm version vào cache key. Khi data thay đổi, tăng version.
csharppublic async Task<List<Product>> GetCategoryProductsAsync(int categoryId, CancellationToken ct)
{
var version = await _cache.GetStringAsync($"category-version:{categoryId}", ct) ?? "0";
var cacheKey = $"products:category:{categoryId}:v{version}";
var cached = await _cache.GetStringAsync(cacheKey, ct);
if (cached is not null)
return JsonSerializer.Deserialize<List<Product>>(cached)!;
var products = await _db.Products.Where(p => p.CategoryId == categoryId).ToListAsync(ct);
await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(products), ct);
return products;
}
// Khi update: tăng version -> key cũ tự hết TTL
public async Task InvalidateCategoryAsync(int categoryId, CancellationToken ct)
{
await _cache.IncrementAsync($"category-version:{categoryId}");
}
Cache Stampede Prevention
Cache stampede (hay thundering herd) xảy ra khi nhiều request cùng lúc gặp cache miss và đồng loạt query DB.
Giải pháp: Locking (Mutex)
csharppublic class StampedeProtectedCache
{
private readonly IDistributedCache _cache;
private readonly IDatabase _redisDb;
private static readonly TimeSpan LockTimeout = TimeSpan.FromSeconds(5);
public async Task<T?> GetOrCreateAsync<T>(
string key,
Func<Task<T>> factory,
TimeSpan expiry,
CancellationToken ct = default) where T : class
{
// 1. Thử đọc cache
var cached = await _cache.GetStringAsync(key, ct);
if (cached is not null)
return JsonSerializer.Deserialize<T>(cached);
// 2. Acquire distributed lock
var lockKey = $"lock:{key}";
var lockValue = Guid.NewGuid().ToString();
var acquired = await _redisDb.StringSetAsync(
lockKey, lockValue, LockTimeout, When.NotExists);
if (!acquired)
{
// Chờ và thử lại - request khác đang build cache
await Task.Delay(100, ct);
cached = await _cache.GetStringAsync(key, ct);
return cached is not null ? JsonSerializer.Deserialize<T>(cached) : null;
}
try
{
// 3. Double-check sau khi có lock
cached = await _cache.GetStringAsync(key, ct);
if (cached is not null)
return JsonSerializer.Deserialize<T>(cached);
// 4. Build cache
var result = await factory();
await _cache.SetStringAsync(key,
JsonSerializer.Serialize(result),
new DistributedCacheEntryOptions
{ AbsoluteExpirationRelativeToNow = expiry },
ct);
return result;
}
finally
{
// 5. Release lock (chỉ nếu vẫn là owner)
await _redisDb.ScriptEvaluateAsync(
"if redis.call('get',KEYS[1])==ARGV[1] then redis.call('del',KEYS[1]) end",
new RedisKey[] { lockKey },
new RedisValue[] { lockValue });
}
}
}
Trade-off Summary
| Quyết định | Option A | Option B | Guideline |
|---|---|---|---|
| TTL ngắn vs dài | Ngắn (1-5 phút): data fresh hơn | Dài (30-60 phút): ít load DB | Tùy data change frequency |
| Local vs Distributed cache | Local: nhanh hơn, đơn giản | Distributed: consistent giữa instances | Dùng cả hai (L1/L2 cache) |
| Invalidation: TTL vs Event | TTL: đơn giản | Event: chính xác hơn | Event cho critical data, TTL cho phần còn lại |
| Serialize: JSON vs Binary | JSON: debug dễ | Binary (MessagePack): nhỏ/nhanh hơn | Binary cho production, JSON cho dev |