Caching Mechanism
Caching is the first step in performance optimization—for data that is read frequently but written infrequently, caching it once can save you a thousand queries.
1. What You'll Learn
@EnableCachingand@Cacheable/@CachePut/@CacheEvictNotes- Cache key strategies and "condition" / "unless" conditional filtering
- Caffeine Local Cache Configuration: TTL / Maximum Capacity / Eviction Policy
- Strategies for Addressing Cache Penetration and Cache Breakthrough Issues
- Alice added Caffeine caching to a popular product list that receives millions of daily visits
2. A True Story of a Performance Engineer
(1) Pain Point: The database is overloaded
At the product launch, Charlie demonstrated OrderFlow's flash sale feature, which instantly triggered 500 thousand requests—all of which flooded the database with queries for popular products. The MySQL connection pool was exhausted, causing the entire system to go down for 10 minutes and resulting in a direct loss of 100 thousand USD. Alice's analysis revealed that product detail queries accounted for 80% of total requests, yet this data remained virtually unchanged for an entire hour.
(2) Solution Using Spring Cache
You can enable caching with just one annotation:
@Cacheable(value = "products", key = "#id")
public Product getProduct(Long id) {
return productRepository.findById(id).orElseThrow();
}
The first query accesses the database; subsequent queries read directly from the cache.
(3) Revenue
After Alice added Caffeine caching to popular products, 80% of queries hit the cache, database load was reduced by a factor of 5, and P99 latency dropped from 500 ms to 5 ms, successfully supporting the next flash sale.
3. Spring Cache Annotation System
(1) Core Annotations
| Description | Function | Typical Scenarios |
|---|---|---|
@Cacheable |
Check the cache first when querying; if a match is found, return the result immediately | Query Operations |
@CachePut |
Execute the method and update the cache | Update operation |
@CacheEvict |
Clear Cache | Delete |
@Caching |
Combining Multiple Cache Operations | Complex Scenarios |
@EnableCaching |
Enable Caching Support | Configuration Class |
graph TD
A["Client Request"] --> B{"@Cacheable<br/>Cache Hit?"}
B -->|Yes| C["Return Cached Data"]
B -->|No| D["Execute Method<br/>Query Database"]
D --> E["Store Result in Cache"]
E --> F["Return Result"]
(1) ▶ Example: @Cacheable Query Cache
@Service
@EnableCaching
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Cacheable(value = "products", key = "#id")
public Product getProduct(Long id) {
log.info("Cache miss, querying database for product {}", id);
return productRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Product", id));
}
@Cacheable(value = "product-list", key = "#keyword + '-' + #page + '-' + #size")
public Page<Product> searchProducts(String keyword, int page, int size) {
return productRepository.findByNameContaining(keyword,
PageRequest.of(page, size));
}
}
Output:
// Execution Successful
(2) ▶ Example: @CachePut updates the cache
@CachePut(value = "products", key = "#result.id")
public Product updateProduct(Long id, UpdateProductRequest request) {
Product product = getProduct(id);
product.setName(request.name());
product.setPrice(request.price());
product.setStock(request.stock());
return productRepository.save(product);
}
Output:
// Execution Successful
(3) ▶ Example: @CacheEvict clears the cache
@CacheEvict(value = "products", key = "#id")
public void deleteProduct(Long id) {
productRepository.deleteById(id);
}
@CacheEvict(value = "product-list", allEntries = true)
public void clearProductListCache() {
log.info("Cleared all product list cache entries");
}
Output:
// Execution Successful
| Note | Execute Method | Update Cache | Applicable Operations |
|---|---|---|---|
@Cacheable |
Execute on cache miss | Update on cache miss | Query |
@CachePut |
Always Run | Always Update | Update |
@CacheEvict |
Always Execute | Clear Specified Cache | Delete |
4. Cache Key Strategy
(1) Key Generation Rules
| Key Strategy | SpEL Expression | Generated Example |
|---|---|---|
| Single Parameter | key = "#id" |
products::123 |
| Multi-parameter Combination | key = "#keyword + '-' + #page" |
product-list::laptop-0 |
| Object Property | key = "#request.category" |
products::electronics |
| Default | Not specified | The hashCode of all parameter combinations |
(1) ▶ Example: Custom KeyGenerator
@Configuration
@EnableCaching
public class CacheConfig {
@Bean("customKeyGenerator")
public KeyGenerator customKeyGenerator() {
return (target, method, params) -> {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getSimpleName()).append(":");
sb.append(method.getName()).append(":");
for (Object param : params) {
sb.append(param != null ? param.toString() : "null").append(".");
}
return sb.toString();
};
}
}
// Usage
@Cacheable(value = "products", keyGenerator = "customKeyGenerator")
public Product getProduct(Long id) { /* ... */ }
Output:
// Execution Successful
(2) Conditional Filtering
(2) ▶ Example: condition and unless
// Only cache products with price > 100
@Cacheable(value = "expensive-products", key = "#id",
condition = "#id != null")
public Product getExpensiveProduct(Long id) { /* ... */ }
// Do not cache if result has stock == 0
@Cacheable(value = "products", key = "#id",
unless = "#result.stock == 0")
public Product getProduct(Long id) { /* ... */ }
Output:
// Execution Successful
| Parameter | Execution Timing | Meaning |
|---|---|---|
condition |
Before the method is executed | Cached only if conditions are met |
unless |
After the method is executed | If the condition is met, do not cache |
5. Caffeine Local Cache Configuration
(1) Caffeine Features
| Feature | Description |
|---|---|
| High Performance | 30% higher throughput than Guava Cache |
| Asynchronous Refresh | Supports asynchronous background refresh of expired cache |
| Statistics | Hit Rate, Load Time, etc. |
| Window TinyLFU | Optimal Elimination Algorithm |
(1) ▶ Example: Caffeine Cache Configuration
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
Output:
// Execution Successful
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(caffeineCacheBuilder());
return manager;
}
@Bean
public Caffeine<Object, Object> caffeineCacheBuilder() {
return Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(30)) // TTL: 30 min after write
.expireAfterAccess(Duration.ofHours(1)) // Evict after 1 hour idle
.maximumSize(10_000) // Max 10,000 entries
.recordStats(); // Enable statistics
}
// Per-cache configuration
@Bean
public CacheManager multiCacheManager() {
Map<String, CaffeineCache> caches = Map.of(
"products", buildCache(Caffeine.newBuilder()
.expireAfterWrite(Duration.ofHours(1))
.maximumSize(5_000)),
"product-list", buildCache(Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(10))
.maximumSize(1_000))
);
return new SimpleCacheManager() {{
setCaches(caches.values());
}};
}
private CaffeineCache buildCache(Caffeine<Object, Object> builder) {
return new CaffeineCache("cache", builder.build());
}
}
| Configuration Option | Description | Recommended Value |
|---|---|---|
expireAfterWrite |
Expiration time after write | 30 min - 1 h |
expireAfterAccess |
Time to expire after last access | 1h - 24h |
maximumSize |
Maximum number of cache entries | Adjust based on memory |
recordStats |
Enable Statistics | Enable in Development Environment |
6. Cache Penetration and Cache Breakthrough
(1) Common Caching Issues
| Problem | Cause | Consequence | Solution |
|---|---|---|---|
| Cache Penetration | Querying Non-Existent Data | Querying the Database Every Time | Caching Null Values / Bloom Filters |
| Cache Breakthrough | A surge of requests at the exact moment a hot key expires | Sudden spike in database load | Mutual exclusion lock / Never expires + asynchronous refresh |
| Cache Avalanche | Massive Number of Keys Expiring Simultaneously | Sudden Increase in Database Load | Expiration Time Plus Random Offset |
graph TD
A["Cache Penetration<br/>Query non-existent data"] --> B["Solution: Cache null value"]
C["Cache Breakdown<br/>Hot key expires"] --> D["Solution: Mutex lock / async refresh"]
E["Cache Avalanche<br/>Many keys expire at once"] --> F["Solution: Random TTL offset"]
(1) ▶ Example: Caching null values to prevent penetration
@Cacheable(value = "products", key = "#id", unless = "#result == null && #result != ''")
public Product getProduct(Long id) {
return productRepository.findById(id).orElse(null);
}
// Alternative: Cache null with short TTL
@Bean
public CacheManager cacheManager() {
return new CaffeineCacheManager() {{
setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(5))
.maximumSize(10_000));
}};
}
Output:
// Execution Successful
7. Comprehensive Example: Complete Implementation of the OrderFlow Product Cache
// CacheConfig.java
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
Map<String, Cache> caches = new HashMap<>();
caches.put("products", Caffeine.newBuilder()
.expireAfterWrite(Duration.ofHours(1))
.maximumSize(5_000)
.recordStats()
.build());
caches.put("product-list", Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(10))
.maximumSize(500)
.build());
caches.put("product-stats", Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(5))
.maximumSize(100)
.build());
SimpleCacheManager manager = new SimpleCacheManager();
manager.setCaches(caches.values().stream()
.map(c -> new CaffeineCache(c.getClass().getName(), c))
.toList());
return manager;
}
}
// ProductService.java with caching
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Cacheable(value = "products", key = "#id")
public Product getProduct(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Product", id));
}
@Cacheable(value = "product-list", key = "#keyword + '-' + #pageable.pageNumber + '-' + #pageable.pageSize")
public Page<Product> searchProducts(String keyword, Pageable pageable) {
return productRepository.findByNameContaining(keyword, pageable);
}
@CachePut(value = "products", key = "#result.id")
@CacheEvict(value = "product-list", allEntries = true)
public Product updateProduct(Long id, String name, BigDecimal price, Integer stock) {
Product product = getProduct(id);
product.setName(name);
product.setPrice(price);
product.setStock(stock);
return productRepository.save(product);
}
@CacheEvict(value = {"products", "product-list"}, allEntries = true)
public void deleteProduct(Long id) {
productRepository.deleteById(id);
}
}
❓ FAQ
maximumSize and maximumWeight?maximumSize limits the number of entries, while maximumWeight limits the total weight (requires specifying a weigher). If the sizes of the cached objects vary significantly, using maximumWeight is more precise.recordStats() in Caffeine, you can retrieve the hit rate using Cache.stats(). Combine this with Micrometer to register the metrics with Prometheus, and visualize them in Grafana.📖 Summary
@CacheableCheck cache → If cache miss, query database → Write to cache@CachePutAlways executes the method and updates the cache; suitable for update operations@CacheEvictClear the cache;allEntries = trueClear the entire cache- Key policy expressed using SpEL expressions;
condition/unlesscache filtering conditions - Configure Caffeine's TTL, maximum capacity, and eviction policy for a high-performance local cache
- Three Major Caching Issues: Cache Penetration (empty cache values), Cache Breakthrough (mutual exclusion locks), and Cache Avalanche (random TTL)
📝 Exercises
-
Basic Exercise (Difficulty: ⭐): Add the
@Cacheablecache to the product query in OrderFlow, and verify that the SQL log is not printed on the second query (cache hit). -
Advanced Problem (Difficulty ⭐⭐): Configure the Caffeine cache manager to set different TTLs and maximum capacities for different cache spaces. Implement
@CachePutto update the cache when a product is updated, and@CacheEvictto clear the cache when a product is deleted. -
Challenge (Difficulty: ⭐⭐⭐): Implement a two-level cache (L1: Caffeine + L2: Redis). Create a custom
CacheResolverthat determines whether to use the local or distributed cache based on the cache name. Consider the consistency strategy for the two-level cache.



