As we determined earlier, we need to be able to systematically evict out-of-date values from the cache. We do that with cache invalidation.
Whenever the database updates a value, it put an invalidation message on a queue. The cache will process messages from that queue: if it contains the key it will evict the value, otherwise it will do nothing.
Assumptions:
Our invalidation queue does not guarantee in-order delivery.
Our invalidation queue is durable and guarantees at-least-once delivery.
Initial cache invalidation
WriterWriterDatabaseDatabaseCache Invalidation QueueCache Invalidation QueueCacheCacheUpdates dataAdds updated key to queuePolls queueReturns invalidation itemEvicts invalidated key from queuealt[Key not in Cache]Does nothing
Initial cache invalidation solution
Modeling
We are now dealing with multiple processes, cache fill and invalidation, that may interact. Therefore it is necessary to break the processes down into their component steps, which may be executed simultaneously. Also, for context, a Cache Fill describes the process of the Cache requesting data from the Database, the Database responding, and the Cache incorporating that data.
It is now worthwhile to model the cache’s state machine.
3. DatabaseRespondToCacheFillDatabase responds with k1:v0
cache
k1
type:miss
cacheFillStates
k1
state:respondedto
version:0
database
k1: 0
invalidationQueue :
4. DatabaseUpdateValue changes to k1:v2, triggering invalidation
cache
k1
type:miss
cacheFillStates
k1
state:respondedto
version:0
database
k1: 1
invalidationQueue
key: k1
5. CacheHandleInvalidationMessageKey isn't in cache, does nothing
cache
k1
type:miss
cacheFillStates
k1
state:respondedto
version:0
database
k1: 1
invalidationQueue :
6. CacheCompleteFillCache completes fill with old value. k1:v0
cache
k1
version:0
type:hit
cacheFillStates
k1
state:inactive
version:0
database
k1: 1
invalidationQueue :
StutteringNo more updates, and cache remains inconsistent
Clearly we have a race condition between cache invalidation and cache fill. Let’s try to rectify that.
Updated cache invalidation solution
An updated design
So our data has been versioned all along for observability. It’s time to start using the versions in our solution. This isn’t unrealistic, as databases can send snapshot times along with results and invalidations.
Let’s also start sending the data along with the invalidations, so that we can update the cache when things change.
Whenever a cache fill comes back, or an invalidation message is received, we will compare the version we just received to the version in the cache. We will only modify the cache if the version is higher. That way we don’t need to be concerned with race conditions of that sort. Whichever comes back first will be compared to the one that comes back second, and the cache will eventually have the same value.
Updated cache invalidation
WriterWriterDatabaseDatabaseCache Invalidation QueueCache Invalidation QueueCacheCacheUpdates dataAdds updated key andversioned data to queuePolls queueReturns invalidation itemalt[Key not in Cache]Does nothingalt[Key in Cache]Does nothingalt[Cache data olderthan invalidation]Replaces old versionwith data from message
Modeling
We should update the state machines to:
InactiveCacheStartReadThroughFillDatabaseRespondToCacheFillCacheCompleteFillCacheFailFillCacheIgnoreFillfill version is older than stored version
InvalidationMessageOnQueueCacheHandleInvalidationMessageCacheIgnoreInvalidationMessagemessage version is older than stored version
4. DatabaseUpdateAn invalidation message is sent with k1:v1
cache
k1
type:miss
cacheFillStates
k1
version:0
state:respondedto
database
k1: 1
invalidationQueue
key: k1
version: 1
5. CacheIgnoreInvalidationMessageThe invalidation message is ignored because k1 is not in cache
cache
k1
type:miss
cacheFillStates
k1
version:0
state:respondedto
database
k1: 1
invalidationQueue :
6. CacheCompleteFillThe outdated k1:v0 is loaded as fill comes back
cache
k1
version:0
type:hit
cacheFillStates
k1
version:0
state:inactive
database
k1: 1
invalidationQueue :
StutteringNo more updates, and cache remains inconsistent
Summary
Our main problem remaining is that cache invalidation messages are ignored if the key is not in the cache. In this case, a cache fill can be completed incorrectly with the old value. More broadly, the solution doesn’t take ongoing cache fills into consideration. We should address this in our next design.