Spring Boot: 缓存机制

最后更新:2026-08-26

缓存是性能优化的第一招——读多写少的数据,一次缓存省千次查询。

1. 你将学到


2. 一个性能工程师的真实故事

(1) 痛点:数据库被打爆

Charlie 在产品发布会上展示了 OrderFlow 的秒杀功能,瞬间涌入 500 thousand 请求,全部打到数据库查询热门商品。MySQL 连接池耗尽,整个系统宕机 10 分钟,直接损失 100 thousand USD。Alice 分析发现,商品详情查询占了总请求的 80%,但这些数据 1 小时内几乎不变。

(2) Spring Cache 的解法

一条注解就能加缓存:

JAVA
@Cacheable(value = "products", key = "#id")
public Product getProduct(Long id) {
    return productRepository.findById(id).orElseThrow();
}

第一次查询走数据库,后续直接从缓存读取。

(3) 收益

Alice 为热门商品添加 Caffeine 缓存后,80% 的查询命中缓存,数据库压力降低 5 倍,P99 延迟从 500ms 降到 5ms,成功支撑了下次秒杀活动。


3. Spring Cache 注解体系

(1) 核心注解

注解 作用 典型场景
@Cacheable 查询时先查缓存,命中直接返回 查询操作
@CachePut 执行方法并更新缓存 更新操作
@CacheEvict 清除缓存 删除操作
@Caching 组合多个缓存操作 复杂场景
@EnableCaching 启用缓存支持 配置类
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"]

▶ 示例: @Cacheable 查询缓存

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));
    }
}

输出:

TEXT 📖 仅展示
// 执行成功

▶ 示例: @CachePut 更新缓存

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);
}

输出:

TEXT 📖 仅展示
// 执行成功

▶ 示例: @CacheEvict 清除缓存

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");
}

输出:

TEXT 📖 仅展示
// 执行成功
注解 是否执行方法 是否更新缓存 适用操作
@Cacheable 缓存未命中时执行 缓存未命中时更新 查询
@CachePut 总是执行 总是更新 更新
@CacheEvict 总是执行 清除指定缓存 删除

4. 缓存 Key 策略

(1) Key 生成规则

Key 策略 SpEL 表达式 生成示例
单参数 key = "#id" products::123
多参数组合 key = "#keyword + '-' + #page" product-list::laptop-0
对象属性 key = "#request.category" products::electronics
默认 不指定 所有参数组合的 hashCode

▶ 示例: 自定义 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) { /* ... */ }

输出:

TEXT 📖 仅展示
// 执行成功

(2) 条件过滤

▶ 示例: condition 和 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) { /* ... */ }

输出:

TEXT 📖 仅展示
// 执行成功
参数 执行时机 含义
condition 方法执行前 满足条件才缓存
unless 方法执行后 满足条件不缓存

5. Caffeine 本地缓存配置

(1) Caffeine 特性

特性 说明
高性能 比 Guava Cache 吞吐量高 30%
异步刷新 支持后台异步刷新过期缓存
统计信息 命中率、加载时间等
Window TinyLFU 最优淘汰算法

▶ 示例: Caffeine 缓存配置

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

输出:

TEXT 📖 仅展示
// 执行成功
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());
    }
}
配置项 含义 推荐值
expireAfterWrite 写入后过期时间 30min - 1h
expireAfterAccess 最后访问后过期时间 1h - 24h
maximumSize 最大缓存条目数 根据内存调整
recordStats 启用统计信息 开发环境开启

6. 缓存穿透与缓存击穿

(1) 常见缓存问题

问题 原因 后果 解决方案
缓存穿透 查询不存在的数据 每次都打数据库 缓存空值 / 布隆过滤器
缓存击穿 热点 key 过期瞬间大量请求 数据库压力骤增 互斥锁 / 永不过期 + 异步刷新
缓存雪崩 大量 key 同时过期 数据库压力骤增 过期时间加随机偏移
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"]

▶ 示例: 缓存空值防止穿透

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));
    }};
}

输出:

TEXT 📖 仅展示
// 执行成功

7. 综合示例:OrderFlow 商品缓存完整实现

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);
    }
}

❓ 常见问题

Q @Cacheable 在同类方法互调时生效吗?
A 不生效。和 @Transactional 一样,@Cacheable 基于 AOP 代理,同类方法互调不走代理。需要拆分类或注入代理。
Q Caffeine 和 Redis 缓存该选哪个?
A Caffeine 是本地缓存(单进程),速度极快但不支持集群共享。Redis 是分布式缓存,所有实例共享但需要网络开销。通常两者组合使用(L1 Caffeine + L2 Redis)。
Q 缓存和数据库一致性如何保证?
A 常用策略:1)Cache Aside(先更新数据库,再删缓存);2)Write Through(同步写缓存和数据库);3)Write Behind(先写缓存,异步写数据库)。推荐 Cache Aside + 延迟双删。
Q maximumSize 和 maximumWeight 有什么区别?
A maximumSize 限制条目数量,maximumWeight 限制总权重(需要指定 weigher)。如果缓存对象大小差异大,用 maximumWeight 更精确。
Q 如何监控缓存命中率?
A Caffeine 启用 recordStats() 后,通过 Cache.stats() 获取命中率。配合 Micrometer 注册到 Prometheus,在 Grafana 可视化。
Q @CachePut 和 @Cacheable 可以用在同一个方法上吗?
A 不建议。@Cacheable 可能跳过方法执行(缓存命中时),@CachePut 总是执行方法并更新缓存,两者语义冲突。

📖 小节


📝 作业

  1. 基础题(难度⭐):为 OrderFlow 的商品查询添加 @Cacheable 缓存,验证第二次查询不打印 SQL 日志(缓存命中)。

  2. 进阶题(难度⭐⭐):配置 Caffeine 缓存管理器,为不同缓存空间设置不同的 TTL 和最大容量。实现 @CachePut 更新商品时同步更新缓存,@CacheEvict 删除商品时清除缓存。

  3. 挑战题(难度⭐⭐⭐):实现两级缓存(L1 Caffeine + L2 Redis),自定义 CacheResolver 根据缓存名决定使用本地还是分布式缓存,思考两级缓存的一致性策略。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

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

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