A Spring Boot 4.1.1 module whose test suite is the evidence for the article: 20 tests producing 22 transcripts under docs/output/, plus 12 documentation chapters. Findings the build pins: - @EnableCaching has no exposeProxy attribute; the widely-copied @EnableCaching(exposeProxy = true) does not compile. - Two methods sharing a cache name and an argument type share a key space, and one silently serves the other's answers. - The documented cache-provider detection order does not match CacheType's enum order in 4.1.1: COUCHBASE before INFINISPAN, and CACHE2K before CAFFEINE. - beforeInvocation = true is NOT deferred by TransactionAwareCacheManagerProxy on 7.0.9 - doEvict picks evictIfPresent, which the decorator does not intercept. - Four of five invalid declarations start a clean context and throw at the first call. - Caffeine on the classpath silently displaces the simple provider.
3.0 KiB
← conditions and nulls · next: providers and TTL →
7. sync = true, and the async return types
The stampede
docs/output/11-stampede.txt, sixteen threads hitting one cold key, method sleeping 300 ms:
@Cacheable("reports") -> 16 invocations
@Cacheable("syncedReports", sync = true) -> 1 invocation
Nothing is wrong with the unsynchronised version — it is doing exactly what it was told. Every thread that arrives during the 300 ms window finds a miss and runs the method. The problem is when this happens: right after a deployment, right after an eviction, right when the cache would have been most valuable. A cache that collapses under the load it was added to survive is a well-known way to turn a slow endpoint into an outage.
sync = true makes one caller compute while the others block on the same computation. It is
implemented on top of Cache.get(key, Callable), so the provider has to support it; all the
CacheManager implementations in the framework do.
What sync = true will not tolerate
Four restrictions, all enforced at the first call rather than at startup. Real messages from
docs/output/20-invalid-declarations.txt:
| Declaration | Message |
|---|---|
unless alongside sync |
A sync=true operation does not support the unless attribute on ... |
| two cache names | A sync=true operation is restricted to a single cache on ... |
| combined with another cache operation | A sync=true operation cannot be combined with other cache operations on ... |
All three are IllegalStateException, thrown from the interceptor, on a context that started
cleanly. An integration test that calls the method once is the cheapest possible insurance.
CompletableFuture and reactive types
Since Spring Framework 6.1 the cache annotations understand CompletableFuture, Mono and
Flux. The interceptor unwraps the container and caches the emitted value.
The cache has to support future-based retrieval. ConcurrentMapCacheManager adapts on its own.
CaffeineCacheManager does not, unless you say so — and the way it tells you is
docs/output/17-async-cache-mode-missing.txt:
cacheManager : org.springframework.cache.caffeine.CaffeineCacheManager
The application started cleanly. Nothing warned about anything.
buildAsync("q3") ->
java.lang.IllegalStateException: No Caffeine AsyncCache available: set CaffeineCacheManager.setAsyncCacheMode(true)
The fix is one line on the manager:
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setAsyncCacheMode(true);
With it on, docs/output/12-async-return-types.txt shows two calls and one supplier invocation.
Be careful how far you take this. The reference documentation's own caveat is worth quoting:
annotation-driven caching "is not appropriate for sophisticated reactive interactions involving
composition and back pressure" — a @Cacheable Flux stores a pre-collected list, which is
rarely what a streaming endpoint wanted.