Skip to content

Latest commit

 

History

18 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Fancy caching example

Experimenting with Redis caching that avoids the stampede problem, does automatic background cache refresh, and helps to maintain data consistency with clients that do local in-memmory caching on top of Redis caching.

What is this?

This is my experimentation with Clojure implementation for caching with Redis.

This has three goals. First, it tries to avoif the cache stampede problem. When multiple clients make simultaneus queries to the cache for a key that is not yet in cache, typically the cache responds to each query with cache MISS. This causes all clients to build a value (like making queries to DB to look for values to be cached). Eventually all clients get the value (from DB for example) and proceed to push the value to cache. This can cause unnecessary load to system and can cause data inconsitencies.

Second goal is to introduce a background cache refresh. When cache detects that the cached value is close to become expired it can trigger a background process to update the cache before the value is expired. This way the cache can provide fresh value from cache without delay.

Finally, the implementation helps to keep possible client in-memory caches in sync. In the case of cache stampede, it is possible that some of the clients produce a more recent value than others. If the clients save the value they push to Redis in their own in-memory caches for faster serving, they end up serving different values.

Implementation

This implementation tries to meet these goals by instructing only the first client that it should build the value for cache and instruct all other clients to wait a short period and retry. Once the first client has constructed the value and submitted it to cache, the other clients can fetch the cached value. If the first client fails to submit value (maybe it crashed), a second client is selected to perform the value construction.

The implementation has a book keepping where it maintains information about the "leader" client; that means the client that is currently selected to produce a value for missing or stale entry. The implementation utilizes Redis expiration mechanism to clear the leader infor after a certain timeout. This is used to detect a client that has failed to produce a value.

Part of the code is in Lua source that is loaded into Redis as a Redis functions. Note that this requires a Redis version 7 or newer.

How?

This implementation implements a Lua library with two functions, one for reading cache entries and one for storing cache entries. In addition this implementation has a Clojure namespace with function to make cache requests. All the logic with cache updates and waits is incorporated to these two parts. Users of this Clojure library see only one simple init function.

Considerations for future

  • [⎷] Add cache a name and use that name as prefix to allow deploying multiple cache instance in same database
  • [⎷] Add possibility to set default stale and expire times for cache instances so that factories do not need to produce them
  • [⎷] Consider publishing a cache updated message when dcache_set is called: Considered, not doing this as it complicates codebase considerably
  • Improve data consistency by providing means to remove updated values from local caches when cache update message is received
  • [⎷] Consider rejecting dcache_set calls from clients that are not currently leaders (see below)
  • [⎷] Consider using relative times for stale and expiration times handle possible clock differences between client and redis instance
  • Run performance analysis and do stress testing

Handling update conflicts

When client is elected as leader the client ID is saved as a key "leader" in cache entry in Redis. This value is set to have an expiration time so that if the elected leader fails the "leader" value is expunged and the library can elect a new leader.

What happens it the original leader does not crash, but instead it was just delayed. The cache would think that the leader has failed and elects a new leader. Now we have two clients working to produce a value for cache.

What happends when the originally elected client calls dcache_set? The "old" client call to dcache_set detects that the old client is not the leader anymore, and the call returns with status "CONFLICT". The client code then restarts the query process and gets the new value from cache.

License

Copyright © 2025 Jarppe Länsiö.

Available under the terms of the Eclipse Public License 2.0.

About

Experimenting with Redis caching that avoids the stampede problem

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages