skip to Main Content

Problem:
I need to efficiently delete keys from my Redis Cache using a wildcard pattern. I don’t need atomicity; eventual consistency is acceptable.

Tech stack:

  • .NET 6 (async all the way through)
  • StackExchange.Redis 2.6.66
  • Redis Server 6.2.6
  • I currently have ~500k keys in Redis.
  • I’m not able to use RedisJSON for various reasons

Example:
I store the following 3 STRING types with keys:

dailynote:getitemsforuser:region:sw:user:123
dailynote:getitemsforuser:region:fl:user:123
dailynote:getitemsforuser:region:sw:user:456
...

where each STRING stores JSON like so:

> dump dailynote:getitemsforuser:region:fl:user:123
"{"Name":"john","Age":22}"

The original solution used the KeysAsync method to retrieve the list of keys to delete via a wildcard pattern. Since the Redis Server is 6.x, the SCAN feature is being used by KeysAsync internally by the StackExchange.Redis nuget.

Original implementation used a wildcard pattern dailynote:getitemsforuser:region:*. As one would expect, this solution didn’t scale well and we started seeing RedisTimeoutExceptions.

I’m aware of the "avoid this in PROD if you can" and have seen Marc Gravell respond to a couple other questions/issues on SO and StackExchange.Redis GitHub. The only potential alternative I could think of is to use a Redis SET to "track" each RedisKey and then retrieve the list of values from the SET (which are the keys I need to remove). Then delete the SET as well as the returned keys.

Potential Solution?:

  1. Create a Redis SET with a key of dailynote:getitemsforuser with a value which is the key of the form dailynote:getitemsforuser:region:XX...

The SET would look like:

dailynote:getitemsforuser (KEY)
    dailynote:getitemsforuser:region:sw:user:123 (VALUE)
    dailynote:getitemsforuser:region:fl:user:123 (VALUE)
    dailynote:getitemsforuser:region:sw:user:456 (VALUE)
    ...

I would still have each individual STRING type as well:

dailynote:getitemsforuser:region:sw:user:123
dailynote:getitemsforuser:region:fl:user:123
dailynote:getitemsforuser:region:sw:user:456
...
  1. when it is time to do the "wildcard" remove, I get the members of the dailynote:getitemsforuser SET, then call RemoveAsync passing the members of the set as the RedisKey[]. Then call RemoveAsync with the key of the SET (dailynote:getitemsforuser)

I’m looking for feedback on how viable of a solution this is, alternative ideas, gotchas, and suggestions for improvement. TIA

UPDATE
Added my solution I went with below…

2

Answers


  1. Chosen as BEST ANSWER

    I ended up using the solution I suggested above where I use a Redis SET with a known/fixed key to "track" each of the necessary keys.

    When a key that needs to be tracked is added, I call StackExchange.Redis.IDatabase.SetAddAsync (SADD) while calling StackExchange.Redis.IDatabase.HashSetAsync (HSET) for adding the "tracked" key (along with its TTL).

    When it is time to remove the "tracked" key, I first call StackExchange.Redis.IDatabase.SetScanAsync (SSCAN) (with a page size of 250) iterating on the IAsyncEnumerable and call StackExchange.Redis.IDatabase.KeyDeleteAsync (HDEL) on chunks of the members of the SET. I then call StackExchange.Redis.IDatabase.KeyDeleteAsync on the actual key of the SET itself.

    Hope this helps someone else.


  2. The big problem with both KEYS and SCAN with Redis is that they require a complete scan of the massive hash table that stores every Redis key. Even if you use a pattern, it still needs to check each entry in that hash table to see if it matches.

    Assuming you are calling SADD when you are also setting the value in your key—and thus avoiding the call to SCAN—this should work. It is worth noting that calls to SMEMBERS to get all the members of a Set can also cause issues if the Set is big. Redis—being single-threaded—will block while all the members are returned. You can mitigate this by using SSCAN instead. StackExchange.Redis might do this already. I’m not sure.

    You might also be able to write a Lua script that reads the Set and UNLINKs all the keys atomically. This would reduce network but could tie Redis up if this takes too long.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search