How to delete a key or keys from the memory cache basis on particular pattern?

6.7k Views Asked by At

I have a method which generates our cache key which is a string like below:

private static readonly HashSet<string> CacheKeys = new HashSet<string>();

public string Generate(int clientId, int processId)
{
    StringBuilder sb = new StringBuilder();
    sb.Append("data:").Append("c:").Append(clientId).Append("::");
    sb.Append("pi::").Append(processId);

    // do I need to store everything in my CacheKeys?
    // or this can be improved so that I can fullfill below 3 requirements
    // CacheKeys.Add(sb.ToString());
    
    return sb.ToString();
}

And then this string gets cached in MemoryCache as a key along with their value (not shown here as it is not relevant).

_memoryCache.Set<T>(cacheKey, value, memoryCacheOptions);

I have a requirement where I need to delete a specific key or keys from the memory cache depending on below requirement-

  • I need to delete specific processId + clientId key from the memory cache.
  • I need to delete all keys matching specific clientId key from the memory cache regardless of processId.
  • I also need to delete all the keys matching a particular processId key from the memory cache regardless of clientId.

Now after reading on SO and MemoryCache tutorial, it looks like there is no way to iterate over all the keys (or find keys matching particular regex pattern) and remove it from the cache.

Problem Statement

After going through various link it looks like I need to store my cache keys in separate data structure and then use that to remove it.

  • How should I store my cacheKey in such a way in the above method so that I can remove a key basis on above 3 requirements easily by iterating this data structure or any thread safe data structure?
  • Also the input structure which specifies what keys to remove (following above 3 pattern) is coming from some other service and that is not defined as well and I am pretty much open to represent in any format which can help me to do this task efficiently.

Basically I am trying to come up with an example for all my above three patterns by which I can remove the keys efficiently from MemoryCache which uses RemoveByPattern or Remove method. As of now I am not able to figure out on how to prepare an example or test cases for all those three cases which can use my RemoveByPattern or Remove method and clear the keys efficiently.

public void RemoveByPattern(MemoryCache memoryCache, string pattern)
{
    var keysToRemove = CacheKeys
        .Where(k => Regex.IsMatch(k, pattern, RegexOptions.IgnoreCase))
        .ToArray();

    foreach (var ktr in keysToRemove)
        Remove(memoryCache, ktr);
}

public void Remove(MemoryCache memoryCache, string key)
{
    memoryCache.Remove(key);
    CacheKeys.Remove(key);
}

How can I represent my input structure in such a way which can understand my above three requirements and then feed it to RemoveByPattern or Remove method easily?

1

There are 1 best solutions below

7
On

I think you are on the right track. The goal is to make keys in CacheKeys consistent with keys in MemoryCache.

Case 1: When adding a new key

You need to add newly created key to both MemoryCache and CacheKeys.

_memoryCache.Set<T>(cacheKey, value, memoryCacheOptions);
CacheKeys.Add(cacheKey);

Case 2: When removing an existing key

Your implementation is good enough.

Case 3: When a key is expired

When a key is expired from MemoryCache, the key set is inconsistent between MemoryCache and CacheKeys. You need to implement a scheduled task running periodically in background to check and clean up CacheKeys (MemoryCache is the source of truth).

private void ConsolidateKeys()
{
    var invalidKeys = new List<string>();
    foreach (var key in CacheKeys)
    {
        if (!_memoryCache.TryGetValue(key, out var _))
        {
            invalidKeys.Add(key);
        }
    }
    foreach (var key in invalidKeys)
    {
        CacheKeys.Remove(key);
    }
}

Note

You need to consider thread safety of accessing CacheKeys since this is a multithreading scenario. Regarding data structure of CacheKeys, ConcurrentDictionary<TKey, TValue> is recommended over HashSet<T>.


Update

To remove keys efficiently (assuming you mean speed), I will trade space for speed.

It is a bit complex, but you can define a set of processId named ProcessIdMap. The key is processId. The value is a set of clientId with the same processId when generating cache keys. ClientIdMap is similar. (pseudo code)

ProcessIdMap = Map<processId, Set<clientId>>
ClientIdMap = Map<clientId, Set<cacheKey>>

Case 1: when adding a new key

private static readonly HashSet<string> CacheKeys = new HashSet<string>();
private static readonly Dictionary<string, HashSet<string>> ProcessIdMap = new Dictionary<string, HashSet<string>>();
private static readonly Dictionary<string, HashSet<string>> ClientIdMap = new Dictionary<string, HashSet<string>>();

private string BuildCacheKey(int clientId, int processId)
{
    var sb = new StringBuilder();
    sb.Append("data:").Append("c:").Append(clientId).Append("::");
    sb.Append("pi::").Append(processId);
    return sb.ToString();
}

public string AddToCache(int clientId, int processId)
{
    // Populate processId map
    if (!ProcessIdMap.TryGetValue(processId, out var clientIdSet))
    {
        clientIdSet = new HashSet<string>();
    }
    clientIdSet.Add(clientId);

    // Populate clientId map
    if (!ClientIdMap.TryGetValue(clientId, out var processIdSet))
    {
        processIdSet = new HashSet<string>();
    }
    processIdSet.Add(processId);

    var cacheKey = BuildCacheKey(clientId, processId);
    
    // Populate cache key store
    CacheKeys.Add(cacheKey);

    // Populate memory cache
    _memoryCache.Set<T>(cacheKey, value, memoryCacheOptions);

    return cacheKey;
}

Case 2: when removing an existing key (RemoveClientId method is similar)

public void RemoveProcessId(string processId)
{
    if (ProcessIdMap.TryGetValue(processId, out var clientIds))
    {
        foreach (var clientId in clientIds)
        {
            var cacheKey = BuildCacheKey(clientId, processId);

            // Remove from cache key store
            CacheKeys.Remove(cacheKey);

            // Remove from memory cache
            _memoryCache.Remove(cacheKey);

            // Remove from clientId map
            if (ClientIdMap.TryGetValue(clientId, out var processIdSet))
            {
                processIdSet.Remove(processId);
            }
        }

        // Remove from processId map
        ProcessIdMap.Remove(processId);
    }
}