A couple of weeks ago Sitecore Experience Commerce 10.2 has been released adding support for new commands in code to clear keys in cache stores managed with the Redis cache provider. Unfortunately the commerce website of one of my clients is on a previous version of the platform, Sitecore Experience Commerce 9.3, and these new commands are not available. In this blog post I describe how to implement a custom solution using the StackExchange.Redis library to clear commerce Redis cache keys via code.

Why The Need to Clear Redis Cache Keys

In my project catalog product entities are not directly created in Sitecore Business Tools, instead they are managed in an external product management system and imported in Sitecore Commerce using a custom import process. This process is executed by a commerce engine minion that runs every 10 minutes. Because the editorial activity for a product might last more than 10 minutes in the external product management system, the product import process doesn’t create a new entity version every time a product is updated, but instead edits all existing versions of the product SellableItem entity.

When an existing entity version is updated, its cached object in the Redis cache store doesn’t get automatically deleted or refreshed. The cache policy for the SellableItems cache is configured by default with an expiration period of 2 hours. This means that it might take up to 2 hours to see product updates in Sitecore Commerce. The discrepancy between what is stored in the actual Sitecore Commerce database and what a content author would see in Business Tools (the cached version of the entity) can cause a lot of confusion and potentially introduce data integrity issues. For these reasons, it is very important to be able to clear the cached objects related to an updated product from the Redis cache store as soon as the product data has been updated in the Sitecore Commerce database.

The Custom Solution

Once I realized that a command to clear a cache key in the Redis cache store was not available in Sitecore Commerce 9.3, I reached out to Sitecore Support team and they kindly recommended to implement a custom solution using the StackExchange.Redis library. This library is already referenced in the Sitecore Commerce minion engine, so there is no need to install any new package.

ConnectionMultiplexer Initialization

In order to use this library, a ConnectionMultiplexer object needs to be instantiated to establish a connection with the Redis Cache instance. The Redis caching provider is configured in the config.json file of a Sitecore commerce engine. This configuration can be read using one of the helper methods already available in the solution, thanks to the Serilog.Settings.Configuration library. The following code shows an example of how to initialize a Redis ConnectionMultiplexer object in a custom command class implementation:

class ImportCommand : CommerceCommand
{
    ...
    protected static IDatabase Cache;
    private readonly IConfiguration _configuration;
    protected static CachingSettings CachingSettings = new CachingSettings(); 

    public ImportCommand(..., IConfiguration configuration)
    {
        ...
        _configuration = configuration;
        _configuration.GetSection("Caching").Bind(CachingSettings);
        Cache = Connection.GetDatabase();
    }

    ...
  
    private static Lazy<ConnectionMultiplexer> lazyConnection = new Lazy<ConnectionMultiplexer>(() =>
    {
        string cacheConnection = CachingSettings.Redis.Options.Configuration;
        return ConnectionMultiplexer.Connect(cacheConnection);
    });

    private static ConnectionMultiplexer Connection
    {
        get
        {
            return lazyConnection.Value;
        }
    }
}

The code above uses the CachingSettings class defined in the Sitecore.Commerce.Core library to store the Redis instance configuration read from the configuration file system.

Custom Task to Find and Delete Redis Cache Keys for a Product

The solution that I implemented leverage the StackExchange.Redis library to execute two main steps: first, identify the cache keys that match a particular pattern, and second, delete these identified keys.

Every product might have multiple cached entity versions and each version has usually three cache keys per version stored in different Redis caches with the following patterns:

  • Entity-SellableItem-<productId>-<version> – in the SellableItems cache
  • Entity-LocalizationEntity-SellableItem-<productId>-<version> in the LocalizationEntities cache
  • Entity-VersioningEntity-SellableItem-<productId>-<version> in the VersioningEntities cache

The StackExchange.Redis library offers the possibility to query a Redis database for keys using a specific pattern, using the GetServer(endpoint).Keys() method available in the Redis ConnectionMultiplexer object. In my case, I searched for keys with the Entity-SellableItem-{productAmsId}- pattern that would return cache keys in all three caches mentioned above. The following code shows the implementation of a custom asynchronous task to find and delete the associated cache keys for a product entity:

private async Task<bool> DeleteRedisCacheProductKeys(string productAmsId, CommerceContext commerceContext)
{
    bool success;
    try
    {
        var endPoint = Connection.GetEndPoints()[0];
        var productKeys = Connection.GetServer(endPoint).Keys(Cache.Database, $"*Entity-SellableItem-{productAmsId}-*", 100).ToArray();
        if (productKeys.Length > 0)
        {
            commerceContext.Logger.LogInformation($"{this.GetType()}: Deleting {productKeys.Length} Redis Cache keys for product {productAmsId}.");
            await Cache.KeyDeleteAsync(productKeys);
        }
        else
        {
            commerceContext.Logger.LogInformation($"{this.GetType()}: No Redis Cache keys found for product {productAmsId}.");
        }
        success = true;
    }
    catch (Exception ex)
    {
        success = false;
        commerceContext.Logger.LogError(ex, $"{this.GetType()}: Failed to delete Redis Cache keys for product {productAmsId}");
    }
    return success;
}

Bonus: Troubleshooting Redis Cache in a Containerized Local Environment

While implementing this solution, I wanted to better understand how commerce Redis cache keys were named. I also wanted to validate that my code was actually deleting existing keys successfully. For these reasons, I used a tool called Redis Commander, a Redis web management tool written in node.js. The tool is distributed in npm, but also in a Linux Docker image ready to use!

This is an example of the tool UI showing the value of a commerce cache key:

Redis Commander tool UI showing the value of a cache key

My project is a containerized solution, so it was very easy to add this tool in my existing docker-compose.yml file adding the new redis-commander service and editing the existing redis service, as follows:

redis:
  image: ${REGISTRY}sitecore-redis:3.0.504-windowsservercore-${WINDOWSSERVERCORE_VERSION}
  platform: windows
  container_name: redis
  hostname: redis
  mem_limit: 512m
  isolation: ${ISOLATION}

redis-commander:
  image: rediscommander/redis-commander:latest
  platform: linux
  container_name: redis-commander
  hostname: redis-commander
  restart: unless-stopped
  environment:
    - REDIS_HOSTS=local:redis:6379
  ports:
    - "44014:8081"

I highly recommend to use this tool while doing development with Redis. It makes the entire experience of working with Redis much easier.

Conclusions

In this blog post I described how to use the StackExchange.Redis library to delete Redis cache keys of commerce entities in a Sitecore Commerce solution. If you have any questions, please don’t hesitate to reach out or comment on this post.

Thank you for reading!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s