I have been using @Cacheable and @CacheEvict in a multithreaded, multi node web app for several years now. As load on the system increases, I am started to notice null
's being returned from the @Cacheable method even though null
is not cached previously. It seems to be due to a race condition between cache evicts happening right before a cache get.
Sanitized sample code below:
@Cacheable(value = "CACHE_NAME", key = "#value", cacheManager = CACHE_MANAGER_NAME, unless = "#result == null")public Bar getBar(Foo foo) { log.info("calculating Bar from Foo: {}", foo); Bar bar = new Bar(foo.getValue() + 1); log.info("calculated Bar: {}", bar); return bar;}
@CacheEvict(value = "CACHE_NAME", key = "#value", cacheManager = CACHE_MANAGER_NAME)public void evict(Foo foo) { log.info("evicted Foo: {}", foo);}
public Bar callingCode(Foo foo) { Bar bar = getBar(foo); log.info("got Bar: {} from Foo: {}", bar, foo);}
Here's what I saw recently in logs:
2022-05-27T05:10:39.976Z: "calculating Bar from Foo: Foo{value=1}"2022-05-27T05:10:39.983Z: "calculated Bar: Bar{value=2)"2022-05-27T05:10:39.985Z: "got Bar: Bar{value=2) from Foo: Foo{value=1}"2022-05-27T05:10:40.203Z: "evicted Foo: Foo{value=1}"2022-05-27T05:10:40.205Z: "got Bar: null from Foo: Foo{value=1}"2022-05-27T05:10:40.209Z: "calculating Bar from Foo: Foo{value=1}"2022-05-27T05:10:40.215Z: "calculated Bar: Bar{value=2)"2022-05-27T05:10:40.226Z: "got Bar: Bar{value=2) from Foo: Foo{value=1}"
My conclusion is that there is some non-thread-safe code in the @Cacheable implementation, such that if it is called at the same time as a @CacheEvict, it may register as a cache hit but then return null
.
I have thought of a few workarounds:
- Add concurrency code around
getBar()
andevict()
methods so that they cannot be called concurrently, and must wait for the other to finish - Add retry code so that if
getBar()
returns null, we evict the cache again and then retrygetBar()
again.
But I wanted to see if there was some feature / setting in this spring framework code that I am just not aware of, that I should be using to prevent this race condition from occurring. I was also surprised to see that I couldn't find this question asked before.