404 Not Found

404 Not Found


nginx

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


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:

JAVA
@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
100%
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

JAVA
@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:

TEXT
// Execution Successful

(2) ▶ Example: @CachePut updates the cache

JAVA
@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:

TEXT
// Execution Successful

(3) ▶ Example: @CacheEvict clears the cache

JAVA
@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:

TEXT
// 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

JAVA
@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:

TEXT
// Execution Successful

(2) Conditional Filtering

(2) ▶ Example: condition and unless

JAVA
// 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:

TEXT
// 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

XML
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

Output:

TEXT
// Execution Successful
JAVA
@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
100%
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

JAVA
@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:

TEXT
// Execution Successful

7. Comprehensive Example: Complete Implementation of the OrderFlow Product Cache

JAVA
// 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

Q Does @Cacheable take effect when methods of the same class call each other?
A No, it does not. Like @Transactional, @Cacheable relies on AOP proxies, and calls between methods of the same class do not go through the proxy. You need to separate the classes or inject a proxy.
Q Which should I choose between Caffeine and Redis for caching?
A Caffeine is a local cache (single-process); it's extremely fast but doesn't support cluster sharing. Redis is a distributed cache; all instances share data, but it incurs network overhead. Typically, the two are used in combination (L1 Caffeine + L2 Redis).
Q How is consistency between the cache and the database ensured?
A Common strategies: 1) Cache Aside (update the database first, then delete the cache); 2) Write Through (write to the cache and database synchronously); 3) Write Behind (write to the cache first, then write to the database asynchronously). We recommend Cache Aside combined with delayed double deletion.
Q What is the difference between maximumSize and maximumWeight?
A 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.
Q How do I monitor the cache hit rate?
A After enabling 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.
Q Can @CachePut and @Cacheable be used on the same method?
A This is not recommended. @Cacheable may skip executing the method (when there is a cache hit), while @CachePut always executes the method and updates the cache; the semantics of the two are in conflict.

📖 Summary


📝 Exercises

  1. Basic Exercise (Difficulty: ⭐): Add the @Cacheable cache to the product query in OrderFlow, and verify that the SQL log is not printed on the second query (cache hit).

  2. Advanced Problem (Difficulty ⭐⭐): Configure the Caffeine cache manager to set different TTLs and maximum capacities for different cache spaces. Implement @CachePut to update the cache when a product is updated, and @CacheEvict to clear the cache when a product is deleted.

  3. Challenge (Difficulty: ⭐⭐⭐): Implement a two-level cache (L1: Caffeine + L2: Redis). Create a custom CacheResolver that determines whether to use the local or distributed cache based on the cache name. Consider the consistency strategy for the two-level cache.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏