در پروژه هایی که چند instance از آن بر روی سرور قرار دارد و از MemoryCache استفاده میکنند، یکی از مشکلات مدیریت کردن MemoryCache آنهاست. به عنوان مثال بر روی یک instance دیتایی درون Memory قرار دارد که با سایر instance ها تفاوت دارد و بعد از مدتی که زمان انقضای آن به پایان میرسد، دیتای درون Memory پاک میشود و مجدد خود را با سایر instance ها به روزرسانی میکند. اگرچه اگر از Redis استفاده کنیم این مشکل وجود ندارد اما سرعت MemoryCache از Redis بیشتر است و در بعضی پروژه ها مجیوریم از هردوی آنها استفاده میکنیم. به طور مثال اگر دیتا درون MemoryCache قرار نداشت از Redis دیتا را دریافت کند و اگر در Redis نبود از SQL دیتای مورد نظر را دریافت کند.

برای حل این مشکل میتوانیم از قابلیت Pub/Sub ردیس استفاده کنیم. نحوه کار Redis Pub/Sub به این صورت است که یک Publisher یک دیتایی را درون یک Channel ارسال میکند و تمامی Subscribe های که به آن Channel متصل هستند آن دیتا را دریافت میکنند. از همین طریق میتوانیم زمانی که دیتایی جدید درون MemoryCache یکی از instance وارد شد یا تغییر کرد، با استفاده از Pub/Sub تمامی instance های موجود را از این تغییر مطلع کنیم و آنها هم Memory Cache خود را به روز رسانی کنند. برای انجام این کار ابتدا یک پروژه به نام RedisPubSub ایجاد میکنیم از نوع Asp.Net Core Web Application. 

پروژه RedisPubSub شامل یک کنترلر و سرویس میباشد که در آن مقدار دیتای به روز رسانی شده درون Channel مربوط به Redis قرار میگیرد و تمامی Subscribe های آن دیتای به روز رسانی شده را دریافت میکنند.

[Route("api/[controller]")]
[ApiController]
public class PublisherController : ControllerBase
{
    private readonly IRedisPublisher _redisPublisher;

    public PublisherController(IRedisPublisher redisPublisher)
    {
        _redisPublisher = redisPublisher;
    }
    [HttpGet]
    public async Task<IActionResult> Get()
    {
        await _redisPublisher.PublishMessage();
        return Ok();
    }
}

سرویس ارسال دیتا بر روی Channel:

public interface IRedisPublisher
{
    Task PublishMessage();
}

public class RedisPublisher : IRedisPublisher
{
    private readonly IDatabaseAsync _databaseAsync;
    public RedisPublisher(IConnectionMultiplexer redisCachingProvider)
    {
        _databaseAsync = redisCachingProvider.GetDatabase();
    }
    public async Task PublishMessage()
    {
        var updatedData = new MemoryCacheDataDto
        {
            CacheKey = "user_information",
            Data = 20
        };
        var redisChannelData = System.Text.Json.JsonSerializer.Serialize(updatedData);
        await _databaseAsync.PublishAsync(RedisChannelConstant.MemoryCache, redisChannelData);
    }
}

همچنین شامل یک HostedService میباشد که به عنوان Subscriber عمل میکند و دیتاهای به روز شده درون Channel را دریافت میکند و درون MemoryCache خود به روز رسانی میکند.

public class RedisSubscriberHostedService : BackgroundService
{
    private readonly IMemoryCache _memoryCache;
    private readonly ILogger<RedisSubscriberHostedService> _logger;
    private readonly ISubscriber _subscriber;
    public RedisSubscriberHostedService(IConnectionMultiplexer connectionMultiplexer, IMemoryCache memoryCache, ILogger<RedisSubscriberHostedService> logger)
    {
        _memoryCache = memoryCache;
        _logger = logger;
        _subscriber = connectionMultiplexer.GetSubscriber();
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await _subscriber.SubscribeAsync(RedisChannelConstant.MemoryCache, (a, updatedData) =>
         {
             var data = System.Text.Json.JsonSerializer.Deserialize<MemoryCacheDataDto>(updatedData);
             _memoryCache.Remove(data.CacheKey);
             _memoryCache.Set(data.CacheKey, data.Data);
             _logger.LogInformation($"MemoryCache update. Key:{data.CacheKey}");
         });
    }
    public override async Task StopAsync(CancellationToken cancellationToken)
    {
        await _subscriber.UnsubscribeAsync(RedisChannelConstant.MemoryCache);
        await base.StopAsync(cancellationToken);
    }
}

کلاس Startup :

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }
    public IConfiguration Configuration { get; set; }
    public void ConfigureServices(IServiceCollection services)
    {
        services.RegisterMultiplexer(Configuration);
        services.AddSingleton<IRedisPublisher, RedisPublisher>();
        services.AddControllers();
        services.AddMemoryCache();
        services.AddHostedService<RedisSubscriberHostedService>();
    }
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseRouting();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapDefaultControllerRoute();
            endpoints.MapGet("/", async context =>
            {
                await context.Response.WriteAsync("Hello World!");
            });
        });
    }
}

اکستنشن متد مربوط به رجیستر کردن Redis :

public static class StartupExtension
{
    public static void RegisterMultiplexer(this IServiceCollection services, IConfiguration configuration)
    {
        var multiplexer = ConnectionMultiplexer.Connect(new ConfigurationOptions
        {
            EndPoints =
            {
                $"{configuration.GetValue<string>("RedisCache:Host")}:{configuration.GetValue<int>("RedisCache:Port")}"
            }
        });
        services.AddSingleton<IConnectionMultiplexer>(multiplexer);
    }
}

کلاس RedisChannelConstant:

public static class RedisChannelConstant
{
    public const string MemoryCache = "memory_cache";
}

کلاس MemoryCacheDataDto :

public class MemoryCacheDataDto
{
    public string CacheKey { get; set; }
    public object Data { get; set; }
}

با این کار اگر API مربوط به Publisher را فراخوانی کنید، دیتای سریالایز شده کلاس MemoryCacheDataDto را درون ردیس پابلیش میکند و HostedService پروژه که به عنوان Subscriber کار میکند، دیتای ارسال شده را دریافت میکند و در MemoryCache خود ذخیره میکند.

نکته: در واقع Publisher و Subscriber هردو درون پروژه اصلی شما قرار دارند.

فرض کنید تعداد instance های شما بر روی سرور 4 عدد میباشد (App_1, App_2, App_3, App_4) و زمانی که دیتایی درون App_1 به روز میشود و دیتای آن را درون Channel ارسال میشود، تمامی instance ها ( App_2, App_3, App_4 ) دیتای به روز شده را دریافت میکنند و درون MemoryCache خود به روز رسانی میکنند ( همچنین چون خود App_1 به عنوان Subscriber عمل میکند نیز یکبار دیگر دیتای درون MemoryCache خود را به روز رسانی میکند).

میتوانید سورس کد این مطلب را از گیتهاب دانلود کنید.

;)

Powered by Froala Editor

نظرات