پیادهسازی راهحل ⚙️
ما به کتابخانهی StackExchange.Redis برای مدیریت پیامرسانی نیاز داریم.
بیایید با تعریف یک سرویس ساده برای انتشار (publishing) شروع کنیم. این سرویس مسئول اطلاع دادن به بقیهی سیستم است که یک کلید تغییر کرده است. 🔔
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, new RedisValue(key));
logger.LogInformation("Published invalidation for key: {Key}", key);
}
}
حالا هر زمان که یک entity را در Command Handler یا Service آپدیت میکنید، کافی است متد ICacheInvalidator.InvalidateAsync را صدا بزنید. 🚀
public class UpdateUserProfileHandler(
AppDbContext dbContext,
ICacheInvalidator cacheInvalidator,
ILogger<UpdateUserProfileHandler> logger)
{
public async Task Handle(int userId, string newName, CancellationToken ct)
{
// 1. Update the database
var user = await dbContext.Users.FindAsync([userId], ct);
if (user is null)
{
return;
}
user.Name = newName;
await dbContext.SaveChangesAsync(ct);
// 2. Invalidate the cache (Distributed)
var cacheKey = $"user:{userId}";
await cacheInvalidator.InvalidateAsync(cacheKey, ct);
logger.LogInformation("Updated user and invalidated cache for {UserId}", userId);
}
}
Background Listener 🧵
در مرحلهی بعد، به یک background service نیاز داریم که روی هر نود اجرا شود. این سرویس روی کانال Redis مشترک میشود و کلیدها را از HybridCache محلی حذف میکند.
یک نکتهی سریع دربارهی self-publishing:
چون Redis Pub/Sub پیام را برای همهی subscriberها ارسال میکند، نودی که خودش پیام invalidation را منتشر کرده هم دوباره همان پیام را دریافت میکند. در این پیادهسازی، ما دوباره همان کلید را حذف میکنیم. این کار تکراری ولی بیضرر است و کد را ساده نگه میدارد. 🔁
توجه کنید که ما مستقیماً HybridCache را داخل background service تزریق کردهایم. یک جایگزین این است که از IMemoryCache استفاده کنیم، چون همان L1 cache داخل HybridCache است.
public class CacheInvalidationService(
IConnectionMultiplexer connectionMultiplexer,
HybridCache hybridCache,
ILogger<CacheInvalidationService> 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) =>
{
string key = value.ToString();
logger.LogInformation("Invalidating local cache for: {Key}", key);
// This removes the item from the local L1 cache
var task = hybridCache.RemoveAsync(key, stoppingToken);
if (!task.IsCompleted)
{
task.GetAwaiter().GetResult();
}
});
}
}