Add caching: the Spring cache abstraction, keys, eviction timing and the self-invocation trap
A Spring Boot 4.1.1 module whose test suite is the evidence for the article: 19 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. - 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.
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
[← 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.
|
||||
Reference in New Issue
Block a user