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.
76 lines
3.1 KiB
Markdown
76 lines
3.1 KiB
Markdown
[← keys](04-keys.md) · [next: conditions and nulls →](06-conditions-and-nulls.md)
|
|
|
|
# 5. Eviction, and when it happens
|
|
|
|
`@CacheEvict` defaults to `beforeInvocation = false`, which means: evict **after** the method
|
|
returns **normally**. A method that throws does not evict.
|
|
|
|
From `docs/output/08-evict-timing.txt`:
|
|
|
|
```
|
|
price("sku-1") -> 100 (stored price is 100)
|
|
|
|
updatePrice("sku-1", 250, fail=true) threw after writing the new price.
|
|
stored price now : 250
|
|
price("sku-1") : 100 <- the cache still serves the old value
|
|
```
|
|
|
|
The write to the real store happened. The exception came afterwards. The eviction did not run,
|
|
so the cache is now authoritative for a value that no longer exists anywhere else. Nothing will
|
|
correct it: the next reader gets 100, and the one after that, until something evicts the entry or
|
|
the process restarts.
|
|
|
|
`beforeInvocation = true` makes the same failure harmless:
|
|
|
|
```
|
|
updatePriceEvictFirst("sku-2", 250, fail=true) threw the same way.
|
|
price("sku-2") : 250 <- the entry went first, so the next read is honest
|
|
```
|
|
|
|
## Choosing between the three shapes
|
|
|
|
| Shape | Cost | Correct when the write fails? |
|
|
|---|---|---|
|
|
| `@CacheEvict` (default) | one miss on the next read | **no** — stale entry survives |
|
|
| `@CacheEvict(beforeInvocation = true)` | one miss, plus a window where two callers can both miss | yes |
|
|
| `@CachePut` | no miss at all | no — and it writes a value the store may not have |
|
|
|
|
`@CachePut` is the fastest and the most dangerous: it puts *your* computed value into the cache
|
|
without reading the store back, so any transformation the database applies — a trigger, a
|
|
default, a truncation, a generated column — is invisible to every subsequent reader. Use it when
|
|
the method's return value is definitively the new state.
|
|
|
|
`beforeInvocation = true` is the right default for anything that mutates. The extra miss costs
|
|
one lookup; the alternative costs an incident.
|
|
|
|
## `allEntries = true`
|
|
|
|
Clears the region in a single operation rather than key by key, which matters when the region is
|
|
large or remote. Two things to know:
|
|
|
|
- It is a blunt instrument in a shared cache: one bulk import evicts everything every other
|
|
caller warmed.
|
|
- On a distributed provider `clear()` may be an O(n) scan or a whole-keyspace operation. Check
|
|
what your provider does before putting it on a frequently called method.
|
|
|
|
## Errors from the cache itself
|
|
|
|
If the cache provider throws — a Redis timeout, a serialization failure — the default
|
|
`SimpleCacheErrorHandler` rethrows, so a cache outage becomes an application outage. A
|
|
`CacheErrorHandler` registered through `CachingConfigurer` can log and continue instead:
|
|
|
|
```java
|
|
@Configuration
|
|
@EnableCaching
|
|
class CacheConfig implements CachingConfigurer {
|
|
@Override
|
|
public CacheErrorHandler errorHandler() {
|
|
return new SimpleCacheErrorHandler() { /* log and swallow get/put/evict errors */ };
|
|
}
|
|
}
|
|
```
|
|
|
|
That is the right call for a read-through cache in front of a database, and the wrong call when
|
|
the eviction is what keeps two systems consistent — a swallowed evict error is a permanently
|
|
stale entry. Decide per cache, not per application.
|