In projects where several instances are on the server and use MemoryCache , one of the problems is managing their MemoryCache. For example, in one instance there is data inside the Memory that is different from other instances, and after the expiration time, the data inside the Memory is erased and updates itself again with the other instances. Although this is not a problem if we use RedisMemoryCache is faster than Redis and in some projects, we have to use both. For example, if the data is not in MemoryCache, it will receive the data from Redis, and if it is not in Redis, it will receive the data from SQL.

To solve this problem, we can use the Pub/Sub Redis feature. How Redis Pub/Sub works are that a Publisher sends data into a Channel and all subscribers connected to that Channel receive that data. In this way, when new data is entered or changed in the Memory Cache of one of the instances, we can use Pub/Sub to inform all existing instances of this change and they will also update their Memory Cache. To do this, first, create a solution called RedisPubSub of type Asp.Net Core Web Application.

The RedisPubSub project includes a controller and service in which the amount of updated data is placed inside the Redis Channel and all subscribers receive the updated data.

[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();
    }
}

Data transmission service on 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);
    }
}

Also, project includes a HostedService that acts as a Subscriber and receives updated data within the Channel and updates it within its 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 class for Publisher project:

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!");
            });
        });
    }
}

Extension of the method for registering Radis:

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 class:

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

MemoryCacheDataDto class:

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

Now, if you call the Publisher API, the Publisher service sends the serialized data of the MemoryCacheDataDto class into the Redis channel, and the Subscriber project, which works as a Subscriber, receives the sent data and stores it in its MemoryCache.

Note: Publisher and Subscriber are both within your core project.

Suppose the number of your instances on the server is 4 ( App_1, App_2, App_3, App_4 ) and when the data in App_1  is updated and its data is sent to the Channel, all instances  (App_2, App_3, App_4)  receive updated data and update own MemoryCache   (also because App_1 itself acts as a Subscriber, it updates the data inside its MemoryCache once more ).

You can download the source code of this article from GitHub.

;)

Powered by Froala Editor

Comments