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,45 @@
|
||||
[← README](../README.md) · [next: the three annotations →](02-the-three-annotations.md)
|
||||
|
||||
# 1. What the Spring cache abstraction actually is
|
||||
|
||||
It is an interceptor and a map interface. That is the whole idea, and holding onto it explains
|
||||
almost every surprise later.
|
||||
|
||||
When a bean carries `@Cacheable`, Spring does not modify the class. It places an AOP proxy in
|
||||
front of it and puts a `CacheInterceptor` in the chain. On each call the interceptor:
|
||||
|
||||
1. asks a `KeyGenerator` for a key,
|
||||
2. asks a `Cache` (looked up from a `CacheManager` by name) whether it holds that key,
|
||||
3. returns the stored value if it does, and otherwise calls the real method and stores the result.
|
||||
|
||||
`org.springframework.cache.Cache` is a small interface — `get`, `put`, `evict`, `evictIfPresent`,
|
||||
`clear`, `invalidate`, `retrieve`. Everything you associate with a cache product — expiry, size
|
||||
limits, eviction policy, replication, persistence, statistics — lives behind that interface in a
|
||||
provider. The abstraction itself has none of it. The reference documentation is explicit about
|
||||
this in its "How can I set the TTL/TTI/eviction policy" section: you configure it on the provider.
|
||||
|
||||
## What that buys you
|
||||
|
||||
Portability of the *declaration*, not the behaviour. The same annotated method runs against a
|
||||
`ConcurrentHashMap` in a unit test, Caffeine in one deployment and Redis in another, without the
|
||||
service code changing. That is genuinely useful and it is the main reason to use it.
|
||||
|
||||
## When not to cache
|
||||
|
||||
- **The method is not slow.** A cache turns a 2 ms call into a 0.1 ms call and adds a correctness
|
||||
problem. Measure first.
|
||||
- **The data must be correct right now.** Balances, stock levels, permissions. A cache is a
|
||||
deliberate decision to serve stale data; make it deliberately.
|
||||
- **The hit rate will be low.** A cache keyed on something nearly unique — a search phrase, a
|
||||
request id — is a memory leak wearing a performance costume.
|
||||
- **The value is huge and the memory budget is not.** On the default provider nothing evicts.
|
||||
|
||||
`docs/output/01-basics.txt` has the smallest possible demonstration: 200 ms, then 0 ms, with the
|
||||
repository's invocation counter proving the method body did not run the second time.
|
||||
|
||||
## The two caches people confuse
|
||||
|
||||
If you are using JPA, you already have caching whether you asked for it or not: the persistence
|
||||
context (first level) and possibly Hibernate's second-level cache. They are a different thing
|
||||
from this, at a different layer, with different failure modes.
|
||||
[Chapter 9](09-versus-hibernate-l2.md) is the comparison.
|
||||
Reference in New Issue
Block a user