Modern Architecture
& Coding Solutions

Redis 分布式缓存实战:从 Spring Cache 到缓存雪崩/穿透/击穿解决方案

从 Spring Boot 3.5 集成到高并发场景下的缓存三大杀手,一篇讲透

缓存是互联网系统的“银弹”吗?几乎每个 Java 后端开发者都听过这句话:“缓存能解决 80% 的性能问题。”但很少有人告诉你,另外 20% 的问题正是由缓存本身引发的——雪崩、穿透、击穿,这三个词足以让任何一个高并发系统在瞬间崩溃。

2026 年,缓存已经不再是“把数据放进去、取出来”那么简单。随着微服务架构的普及和业务规模的膨胀,分布式缓存已经成为系统稳定性的核心命脉。Spring Boot 3.5 对 Spring Cache 抽象层做了进一步优化,支持了更细粒度的缓存控制和响应式缓存集成,但底层的挑战从未改变。

本文将从 Spring Boot 3.5 集成 Redis 开始,带你一步步构建生产级缓存系统,然后深入剖析缓存雪崩、穿透、击穿的成因与解决方案,让你在面对高并发冲击时心中有数。

一、Spring Cache 抽象:统一门面背后的力量

1.1 为什么需要缓存抽象层?

在没有 Spring Cache 之前,我们的代码大概是这样的:

@Service
public class ProductService {

    private final RedisTemplate<String, Object> redisTemplate;
    private final ProductRepository productRepository;

    public Product getProduct(Long id) {
        // 1. 先查缓存
        String key = "product:" + id;
        Product cached = (Product) redisTemplate.opsForValue().get(key);
        if (cached != null) {
            return cached;
        }
        // 2. 缓存未命中,查数据库
        Product product = productRepository.findById(id).orElse(null);
        // 3. 写入缓存
        if (product != null) {
            redisTemplate.opsForValue().set(key, product, 30, TimeUnit.MINUTES);
        }
        return product;
    }
}

这个方法在项目里出现几十上百次后,你会发现自己写了一大堆重复代码,而且每个方法的缓存逻辑还不一致——有的设 TTL,有的不设;有的在更新时清缓存,有的直接删 Key。代码的“坏味道”越来越重。

Spring Cache 抽象层通过 AOP 和注解的方式,将缓存的读、写、删除等操作从业务代码中剥离出来,统一由框架管理。你只需要在方法上添加 @Cacheable@CachePut@CacheEvict 注解即可。

1.2 核心注解解析

注解作用执行时机
@Cacheable触发缓存读取方法执行前
@CachePut强制更新缓存(不影响方法执行)方法执行后
@CacheEvict触发缓存删除方法执行后
@Caching组合多个缓存操作方法执行前后
@CacheConfig类级别共享缓存配置

1.3 Spring Cache + Redis 的架构分层

二、实战:Spring Boot 3.5 集成 Redis 缓存

2.1 项目依赖配置

pom.xml 中添加以下依赖:

<dependencies>
    <!-- Spring Boot Starter Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring Boot Starter Cache(提供 @Cacheable 等注解) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>

    <!-- Spring Data Redis(Lettuce 客户端) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

    <!-- 连接池依赖(建议生产环境使用) -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
    </dependency>

    <!-- 可选:JSON 序列化 -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>
</dependencies>

2.2 配置文件 application.yml

spring:
  data:
    redis:
      host: localhost
      port: 6379
      password:  # 若有密码则配置
      database: 0
      lettuce:
        pool:
          max-active: 20
          max-idle: 8
          min-idle: 2
          max-wait: -1ms
        shutdown-timeout: 200ms
  cache:
    type: redis
    redis:
      # 全局缓存过期时间(毫秒),默认永不过期
      time-to-live: 600000  # 10 分钟
      # 是否允许缓存空值(防止缓存穿透)
      cache-null-values: true
      # Key 前缀
      key-prefix: "cache:"
      # 是否使用 Key 前缀
      use-key-prefix: true

2.3 Redis 配置类(自定义序列化)

package com.example.config;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

/**
 * Redis 缓存配置类
 * 启用 Spring Cache 并自定义序列化方式
 */
@Configuration
@EnableCaching  // 启用 Spring Cache 注解支持
public class RedisCacheConfig {

    /**
     * 配置 RedisCacheManager
     * 使用 GenericJackson2JsonRedisSerializer 实现 JSON 序列化
     * 这样可以存储任意类型的对象,且可读性更好
     */
    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        // 创建 ObjectMapper 用于 JSON 序列化
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.activateDefaultTyping(
            LaissezFaireSubTypeValidator.instance,
            ObjectMapper.DefaultTyping.NON_FINAL,
            JsonTypeInfo.As.PROPERTY
        );

        GenericJackson2JsonRedisSerializer serializer = 
            new GenericJackson2JsonRedisSerializer(objectMapper);

        // 配置默认的缓存策略
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            // Key 序列化使用 String(方便查看)
            .serializeKeysWith(
                RedisSerializationContext.SerializationPair.fromSerializer(
                    new StringRedisSerializer()
                )
            )
            // Value 序列化使用 JSON(可读性好,且支持类型信息)
            .serializeValuesWith(
                RedisSerializationContext.SerializationPair.fromSerializer(serializer)
            )
            // 默认过期时间 10 分钟
            .entryTtl(Duration.ofMinutes(10))
            // 允许缓存 null 值(防止穿透)
            .disableCachingNullValues();  // 生产环境建议关闭,或改为 .enableCachingNullValues()

        // 构建 CacheManager
        return RedisCacheManager.builder(connectionFactory)
            .cacheDefaults(config)
            .transactionAware()  // 支持事务
            .build();
    }
}

关键点:使用 GenericJackson2JsonRedisSerializer 时,会自动在 JSON 中保存类型信息(@class 字段),反序列化时不需要额外指定类型。这对缓存多种类型的对象非常有用。

2.4 实战:带缓存的服务层代码

package com.example.service;

import com.example.entity.Product;
import com.example.repository.ProductRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Slf4j
public class ProductService {

    private final ProductRepository productRepository;

    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    /**
     * 根据 ID 查询商品
     * 缓存 Key 规则:product:{id}
     * 缓存过期时间由全局配置决定(10 分钟)
     * 
     * 注意:sync 参数在 Spring Boot 3.5 中已全面支持
     * 高并发场景下,同一时间只有一个线程去加载缓存,其他线程等待
     */
    @Cacheable(value = "product", key = "#id", sync = true)
    public Product getProductById(Long id) {
        log.info("缓存未命中,从数据库查询商品: id={}", id);
        return productRepository.findById(id)
            .orElseThrow(() -> new RuntimeException("商品不存在: " + id));
    }

    /**
     * 更新商品信息,同时更新缓存
     * @CachePut 无论是否命中缓存,都会执行方法并将结果写入缓存
     */
    @CachePut(value = "product", key = "#product.id")
    @Transactional
    public Product updateProduct(Product product) {
        log.info("更新商品并刷新缓存: id={}", product.getId());
        Product existing = productRepository.findById(product.getId())
            .orElseThrow(() -> new RuntimeException("商品不存在: " + product.getId()));
        existing.setName(product.getName());
        existing.setPrice(product.getPrice());
        existing.setStock(product.getStock());
        return productRepository.save(existing);
    }

    /**
     * 删除商品并清除缓存
     * @CacheEvict 会删除指定的缓存条目
     * allEntries = true 会清空该缓存分区下的所有 Key
     */
    @CacheEvict(value = "product", key = "#id", beforeInvocation = false)
    @Transactional
    public void deleteProduct(Long id) {
        log.info("删除商品并清除缓存: id={}", id);
        productRepository.deleteById(id);
    }

    /**
     * 批量清除商品缓存(例如库存批量更新后)
     * 使用 @Caching 组合多个缓存操作
     */
    @org.springframework.cache.annotation.Caching(evict = {
        @CacheEvict(value = "product", key = "#id"),
        @CacheEvict(value = "productList", allEntries = true)
    })
    public void clearProductCache(Long id) {
        log.info("清除商品相关缓存: id={}", id);
        // 通常不需要额外逻辑,仅用于触发缓存清除
    }
}

三、缓存三大杀手:雪崩、穿透、击穿

这是缓存系统最经典的三大难题。理解它们,是构建高可用缓存系统的第一步。

3.1 缓存雪崩(Cache Avalanche)

定义:大量的缓存 Key 在同一时刻集体失效,或者缓存服务整个不可用,导致所有请求直接打到数据库,瞬间压垮数据库。

常见场景

  • 缓存设置了相同的过期时间(例如全部 30 分钟),到了整点集体失效
  • Redis 服务宕机或网络分区
  • 大量 Key 在同一个时间窗口被逐出(内存不足触发 LRU 淘汰)

解决方案

策略说明实现方式
随机过期时间给 TTL 增加随机偏移量,避免同时失效expire = 600 + random(0, 300)
Redis 高可用部署 Redis Cluster 或 Sentinel 集群主从 + 哨兵 / 集群模式
本地缓存兜底在应用内存中缓存热点数据,Redis 挂了仍能支撑Caffeine 缓存
限流降级当缓存不可用时,限制对数据库的并发请求Sentinel / Resilience4j

Spring Cache 中实现随机 TTL

@Bean
public RedisCacheManagerBuilderCustomizer cacheManagerBuilderCustomizer() {
    return builder -> {
        // 为不同缓存分区设置不同的过期时间
        builder.withCacheConfiguration("product",
            RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(10 + new Random().nextInt(5)))
        );
        builder.withCacheConfiguration("productList",
            RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(5 + new Random().nextInt(3)))
        );
    };
}

3.2 缓存穿透(Cache Penetration)

定义:查询一个根本不存在的数据(例如查询 ID = -1 的用户),缓存没有命中,数据库也没有查到,每次请求都会穿透缓存直达数据库。如果这种请求量很大(例如恶意攻击),数据库会被瞬间打满。

常见场景

  • 查询不存在的数据,如 GET /api/user/-1
  • 查询超长 ID 或随机字符串,数据库无论如何都查不到
  • 爬虫或攻击者构造大量不存在的 Key 刷接口

解决方案

策略说明适用场景
缓存空对象null 也缓存起来,TTL 设置较短(如 5 分钟)容易实现,但会消耗少量内存
布隆过滤器(Bloom Filter)预先将所有存在的 Key 存入布隆过滤器,不存在则直接拦截内存高效,但有极低的误判率
请求参数校验在网关层校验参数合法性(如 ID 不能为负数)最基础的防护

缓存空对象的实现(Spring Cache 默认支持):

spring:
  cache:
    redis:
      cache-null-values: true  # 允许缓存 null
@Service
public class UserService {

    @Cacheable(value = "user", key = "#id")
    public User getUser(Long id) {
        // 如果数据库查不到,返回 null,Spring Cache 会缓存一个空对象
        // 但需要注意 TTL 应该比正常数据短
        return userRepository.findById(id).orElse(null);
    }
}

布隆过滤器实现(Google Guava)

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>33.3.0-jre</version>
</dependency>
@Component
public class BloomFilterService {

    private final BloomFilter<Long> bloomFilter;

    @PostConstruct
    public void init() {
        // 创建布隆过滤器:预计 100 万数据,误判率 1%
        bloomFilter = BloomFilter.create(
            Funnels.longFunnel(),
            1_000_000,
            0.01
        );
        // 从数据库加载所有存在的用户 ID 到布隆过滤器
        List<Long> userIds = userRepository.findAllIds();
        userIds.forEach(bloomFilter::put);
    }

    public boolean likelyExists(Long id) {
        return bloomFilter.mightContain(id);
    }
}

3.3 缓存击穿(Cache Breakdown)

定义:某个热点 Key 在缓存过期的瞬间,恰好有大量并发请求同时查询该 Key。这些请求同时发现缓存未命中,全部去数据库查询,导致数据库压力暴增。

常见场景

  • 热门商品详情页(如秒杀商品),缓存过期瞬间
  • 热点新闻详情页,缓存过期瞬间
  • 明星/大 V 的用户信息页

解决方案

策略说明实现
互斥锁(Mutex)只允许一个线程去加载数据,其他线程等待Redis 分布式锁
永不过期(逻辑过期)缓存不设 TTL,由后台任务异步刷新定时任务 + 双重检查
提前预热在缓存过期前主动刷新(如通过消息通知)定时任务

互斥锁方案(推荐)

@Service
@Slf4j
public class ProductServiceWithLock {

    private final StringRedisTemplate stringRedisTemplate;
    private final ProductRepository productRepository;

    /**
     * 使用 Redis 分布式锁防止缓存击穿
     * 
     * 核心思路:
     * 1. 先查缓存,命中则直接返回
     * 2. 缓存未命中,尝试获取分布式锁
     * 3. 获取锁的线程负责加载数据并写入缓存
     * 4. 未获取锁的线程等待,然后重新从缓存获取
     */
    public Product getProductWithLock(Long id) {
        String cacheKey = "product:" + id;
        String lockKey = "lock:product:" + id;

        // 1. 先查缓存
        Product cached = getFromCache(cacheKey);
        if (cached != null) {
            return cached;
        }

        // 2. 尝试获取分布式锁(使用 Redisson 或 Redis SETNX)
        String lockValue = UUID.randomUUID().toString();
        Boolean acquired = stringRedisTemplate.opsForValue()
            .setIfAbsent(lockKey, lockValue, Duration.ofSeconds(5));

        if (Boolean.TRUE.equals(acquired)) {
            try {
                // 3. 双重检查:获得锁后再次检查缓存(避免重复加载)
                Product cachedAgain = getFromCache(cacheKey);
                if (cachedAgain != null) {
                    return cachedAgain;
                }

                // 4. 从数据库加载数据
                log.info("获取到锁,从数据库加载数据: id={}", id);
                Product product = productRepository.findById(id).orElse(null);

                // 5. 写入缓存
                if (product != null) {
                    stringRedisTemplate.opsForValue()
                        .set(cacheKey, JSON.toJSONString(product), 
                             Duration.ofMinutes(10));
                } else {
                    // 缓存空值,防止穿透
                    stringRedisTemplate.opsForValue()
                        .set(cacheKey, "", Duration.ofMinutes(1));
                }
                return product;
            } finally {
                // 6. 释放锁(使用 Lua 脚本保证原子性)
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
                stringRedisTemplate.execute(
                    new DefaultRedisScript<>(script, Long.class),
                    Collections.singletonList(lockKey),
                    lockValue
                );
            }
        } else {
            // 7. 未获取到锁,短暂等待后重试
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            // 递归调用,重新尝试获取缓存
            return getProductWithLock(id);
        }
    }
}

3.4 三大问题对比速查表

问题成因核心区别解决方案
缓存雪崩大量 Key 同时失效量大面广随机 TTL + Redis 高可用 + 本地缓存
缓存穿透查询不存在的数据无效数据布隆过滤器 + 缓存空值 + 参数校验
缓存击穿热点 Key 失效瞬间并发单个热点互斥锁 + 永不过期 + 提前预热

四、生产环境最佳实践清单

实践项说明优先级
缓存监控监控 Redis 命中率、QPS、内存使用率,设置告警阈值⭐⭐⭐
Key 命名规范使用业务前缀 + 版本号,如 v1:product:{id},便于管理⭐⭐⭐
序列化选型生产环境推荐 Jackson2JsonRedisSerializerProtobuf,避免 JdkSerialization(体积大、不安全)⭐⭐⭐
连接池配置合理设置 max-activemax-idle,避免连接耗尽⭐⭐
缓存热加载系统启动时预热热点数据到缓存中(如首页数据、热门商品)⭐⭐
缓存降级策略当 Redis 不可用时,返回兜底数据或静态页面,而非抛出异常⭐⭐
缓存版本管理数据结构变更时,使用新的 Key 前缀,实现平滑迁移

五、总结

缓存是互联网系统性能优化的利器,但也是一把双刃剑。用好缓存,系统能轻松扛住百万 QPS;用不好缓存,可能一个简单的缓存过期就能把数据库打挂。

本文从 Spring Boot 3.5 的 Spring Cache 抽象层入手,详细介绍了 @Cacheable@CachePut@CacheEvict 等注解的实战用法,以及 Redis 缓存管理器的自定义配置。更重要的是,我们深入剖析了缓存三大杀手——雪崩、穿透、击穿的成因,并提供了互斥锁、布隆过滤器、随机 TTL 等经过生产验证的解决方案。

2026 年的今天,Redis 和 Spring Cache 的组合已经成为 Java 后端开发的标配。但工具只是手段,对缓存原理的理解和对各种异常场景的预判,才是真正区分普通开发者和优秀架构师的分水岭。

系列拓展阅读

参考文献

  1. Spring Data Redis Reference Documentation. https://docs.spring.io/spring-data/redis/docs/current/reference/html/
  2. Spring Cache Abstraction Documentation. https://docs.spring.io/spring-framework/reference/integration/cache.html
  3. Redis Official Documentation. “Redis as a Cache.” https://redis.io/docs/management/scalability/
  4. “Redis 缓存雪崩、穿透、击穿解决方案全解析.” 阿里云开发者社区.
  5. “Cache-Aside Pattern: Practical Guide to Distributed Caching.” Baeldung.
赞(0) 打赏
未经允许不得转载:MACS Dev Hub » Redis 分布式缓存实战:从 Spring Cache 到缓存雪崩/穿透/击穿解决方案

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续提供更多优质内容,让我们一起创建更加美好的网络世界!

支付宝扫一扫

微信扫一扫

登录

找回密码

注册