(ASP.NET Core) Giải quyết bài toán xóa cache phân tán với Redis và HybridCache
Nếu bạn từng làm hệ thống chạy nhiều instance / nhiều pod, chắc chắn sẽ gặp một vấn đề quen thuộc:
Cache rất nhanh… cho đến khi dữ liệu bị sai.
Trong .NET 9, Microsoft giới thiệu HybridCache – kết hợp:
- Cache trong RAM (L1) → cực nhanh
- Cache phân tán (L2) như Redis → dùng chung cho nhiều node
Nghe là thấy ngon rồi, nhưng có một vấn đề: Khi chạy nhiều instance ứng dụng, HybridCache không đồng bộ hóa cache L1 giữa các node. Nếu Node A cập nhật dữ liệu và xóa cache cục bộ, Node B vẫn giữ cache cũ trong L1 và tiếp tục trả dữ liệu “cũ” cho đến khi hết hạn.

Hiện tại thì vẫn đang có một open issues của thư viện này trên dotnet/extensions GitHub repository.
Bài viết này chia sẻ lại cách mình từng xử lý vấn đề này ở các phiên bản .NET trước đây, thời điểm chưa có HybridCache, bằng cách dùng Redis Pub/Sub để các node tự thông báo và xóa cache cho nhau.
Giải pháp: Làm cách nào đó để các node có thể nói chuyện với nhau
Muốn cache đồng bộ, thì khi một node xóa cache, các node khác cũng phải biết.
Mình cần một cơ chế:
- Nhẹ
- Real-time
- Các node đều nghe được
👉 Redis Pub/Sub quá hợp cho việc này.
Nguyên lý
- Publisher: Node thay đổi dữ liệu → sẽ public 1 thông báo thông điệp invalidation kèm key.
- Subscriber: Tất cả node subscribes kênh này và lắng nghe thông điệp.
- Hành động: Khi nhận thông điệp, gọi → xóa cache RAM tương ứng

Nói đơn giản:
“Key này vừa đổi, tụi bây xóa cache đi!”
Triển khai trong .NET
Bạn cần thư viện StackExchange.Redis để xử lý messaging.
1️⃣ Service gửi tín hiệu xóa cache
public interface ICacheInvalidator
{
Task InvalidateAsync(string key, CancellationToken cancellationToken = default);
}
public class RedisCacheInvalidator(
IConnectionMultiplexer connectionMultiplexer,
ILogger<RedisCacheInvalidator> logger)
: ICacheInvalidator
{
private const RedisChannel Channel = RedisChannel.Literal("cache-invalidation");
public async Task InvalidateAsync(string key, CancellationToken cancellationToken = default)
{
var subscriber = connectionMultiplexer.GetSubscriber();
await subscriber.PublishAsync(Channel, key);
logger.LogInformation("Sent cache invalidation for key: {Key}", key);
}
}
Khi update dữ liệu:
await cacheInvalidator.InvalidateAsync("user:123", ct);
2️⃣ Service lắng nghe và xóa cache local
Mỗi instance đều chạy một BackgroundService để nghe Redis:
public class CacheInvalidationListener(
IConnectionMultiplexer connectionMultiplexer,
HybridCache hybridCache,
ILogger<CacheInvalidationListener> logger)
: BackgroundService
{
private const RedisChannel Channel = RedisChannel.Literal("cache-invalidation");
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var subscriber = connectionMultiplexer.GetSubscriber();
await subscriber.SubscribeAsync(Channel, (channel, value) =>
{
var key = value.ToString();
logger.LogInformation("Removing local cache for key: {Key}", key);
hybridCache.RemoveAsync(key, stoppingToken)
.GetAwaiter()
.GetResult();
});
}
}
Mỗi khi có message:
- Node nhận
- Xóa cache RAM
- Request tiếp theo sẽ lấy data mới từ Redis hoặc DB
3️⃣ Đăng ký DI
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
ConnectionMultiplexer.Connect("<REDIS_CONNECTION_STRING>"));
builder.Services.AddHybridCache();
builder.Services.AddSingleton<ICacheInvalidator, RedisCacheInvalidator>();
builder.Services.AddHostedService<CacheInvalidationListener>();
Nếu không muốn tự làm: FusionCache
Nếu bạn:
- Không muốn tự viết Pub/Sub
- Không muốn maintain listener 👉 FusionCache đã làm sẵn hết:
builder.Services.AddFusionCache()
.WithBackplane(
new RedisBackplane(new RedisBackplaneOptions
{
Configuration = "<REDIS_CONNECTION_STRING>"
}))
.AsHybridCache();
Hy vọng bài viết này giúp anh em đỡ dính bug cache khó chịu khi deploy production 🚀 Nếu ai có cách khác hay hơn, cùng chia sẻ nhé 👌
All rights reserved