SpringBoot + Redis 缓存雪崩防护:大量 Key 同时过期?随机偏移 + 互斥重建双保险

问题背景

在使用 Redis 作为缓存时,缓存雪崩是一个常见的性能问题。当大量缓存 Key 在同一时间过期时,会导致以下问题:

  • 数据库压力剧增:所有请求都会直接访问数据库,导致数据库过载
  • 系统响应变慢:数据库处理能力有限,无法处理大量并发请求
  • 服务雪崩:系统可能因此崩溃,影响整个应用的可用性
  • 用户体验下降:响应时间变长,甚至出现超时

缓存雪崩的常见原因包括:

  • 集中过期:大量缓存 Key 设置了相同的过期时间
  • 缓存预热:系统启动时批量加载缓存,设置了相同的过期时间
  • 缓存失效:Redis 服务重启或网络故障导致缓存全部失效
  • 热点数据:热点数据的过期时间集中,导致大量请求同时回源

核心概念

缓存雪崩

缓存雪崩是指在某一时间段内,大量缓存 Key 同时过期或失效,导致所有请求都直接访问数据库,造成数据库压力剧增的现象。

随机偏移

随机偏移是指在设置缓存过期时间时,添加一个随机值,使缓存的过期时间分散,避免集中过期。

互斥重建

互斥重建是指在缓存失效时,只有一个线程去重建缓存,其他线程等待或返回旧值,避免多个线程同时重建缓存导致的数据库压力。

缓存穿透

缓存穿透是指查询一个不存在的数据,导致请求直接访问数据库,且缓存不会被更新。

缓存击穿

缓存击穿是指一个热点 Key 过期,导致大量请求同时访问数据库。

技术实现

方案架构

我们将实现一个集成了缓存雪崩防护功能的 Spring Boot 应用,主要包含以下组件:

  • 缓存服务:提供缓存的基本操作,如 get、set、delete 等
  • 雪崩防护服务:实现随机偏移和互斥重建等防护策略
  • 数据服务:模拟数据库操作,提供数据访问能力
  • 监控服务:监控缓存的使用情况和雪崩防护效果

核心代码实现

1. 缓存配置类

@Configuration
@EnableCaching
public class RedisCacheConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
        template.afterPropertiesSet();
        return template;
    }
    
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(30))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new Jackson2JsonRedisSerializer<>(Object.class)));
        
        return RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
    }
}

2. 雪崩防护服务

@Service
public class CacheAvalancheProtectionService {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    private final String LOCK_PREFIX = "lock:";
    private final long LOCK_EXPIRE = 30000; // 30秒
    
    /**
     * 获取缓存,带雪崩防护
     */
    public <T> T getWithProtection(String key, Class<T> clazz, Supplier<T> dataSupplier, long baseExpire, long randomRange) {
        // 尝试从缓存获取
        T value = get(key, clazz);
        if (value != null) {
            return value;
        }
        
        // 缓存不存在,尝试获取锁
        String lockKey = LOCK_PREFIX + key;
        boolean locked = false;
        try {
            locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", LOCK_EXPIRE, TimeUnit.MILLISECONDS);
            if (locked) {
                // 获取锁成功,从数据源获取数据
                value = dataSupplier.get();
                if (value != null) {
                    // 设置缓存,添加随机偏移
                    long expire = baseExpire + ThreadLocalRandom.current().nextLong(randomRange);
                    set(key, value, expire);
                }
            } else {
                // 获取锁失败,等待一小段时间后重试
                Thread.sleep(50);
                value = get(key, clazz);
            }
        } catch (Exception e) {
            log.error("Get cache with protection failed: {}", e.getMessage());
            // 异常情况下直接从数据源获取
            value = dataSupplier.get();
        } finally {
            if (locked) {
                // 释放锁
                redisTemplate.delete(lockKey);
            }
        }
        
        return value;
    }
    
    /**
     * 获取缓存
     */
    private <T> T get(String key, Class<T> clazz) {
        try {
            Object value = redisTemplate.opsForValue().get(key);
            if (value != null) {
                return JSON.parseObject(JSON.toJSONString(value), clazz);
            }
        } catch (Exception e) {
            log.error("Get cache failed: {}", e.getMessage());
        }
        return null;
    }
    
    /**
     * 设置缓存
     */
    private void set(String key, Object value, long expire) {
        try {
            redisTemplate.opsForValue().set(key, value, expire, TimeUnit.MILLISECONDS);
        } catch (Exception e) {
            log.error("Set cache failed: {}", e.getMessage());
        }
    }
}

3. 缓存服务

@Service
public class CacheService {
    @Autowired
    private CacheAvalancheProtectionService protectionService;
    
    /**
     * 获取缓存,带雪崩防护
     */
    public <T> T get(String key, Class<T> clazz, Supplier<T> dataSupplier) {
        // 基础过期时间:30分钟
        long baseExpire = 30 * 60 * 1000;
        // 随机偏移范围:0-10分钟
        long randomRange = 10 * 60 * 1000;
        
        return protectionService.getWithProtection(key, clazz, dataSupplier, baseExpire, randomRange);
    }
    
    /**
     * 设置缓存,带随机过期时间
     */
    public void set(String key, Object value) {
        // 基础过期时间:30分钟
        long baseExpire = 30 * 60 * 1000;
        // 随机偏移范围:0-10分钟
        long randomRange = 10 * 60 * 1000;
        long expire = baseExpire + ThreadLocalRandom.current().nextLong(randomRange);
        
        try {
            RedisTemplate<String, Object> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
            redisTemplate.opsForValue().set(key, value, expire, TimeUnit.MILLISECONDS);
        } catch (Exception e) {
            log.error("Set cache failed: {}", e.getMessage());
        }
    }
    
    /**
     * 删除缓存
     */
    public void delete(String key) {
        try {
            RedisTemplate<String, Object> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
            redisTemplate.delete(key);
        } catch (Exception e) {
            log.error("Delete cache failed: {}", e.getMessage());
        }
    }
}

4. 数据服务

@Service
public class DataService {
    /**
     * 模拟从数据库获取数据
     */
    public User getUserById(Long id) {
        // 模拟数据库查询延迟
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        // 模拟数据
        User user = new User();
        user.setId(id);
        user.setName("User " + id);
        user.setAge(20 + (int) (id % 30));
        user.setEmail("user" + id + "@example.com");
        return user;
    }
    
    /**
     * 模拟从数据库获取商品列表
     */
    public List<Product> getProducts(int page, int size) {
        // 模拟数据库查询延迟
        try {
            Thread.sleep(150);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        // 模拟数据
        List<Product> products = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            Product product = new Product();
            product.setId((long) (page * size + i + 1));
            product.setName("Product " + (page * size + i + 1));
            product.setPrice(100.0 + (page * size + i + 1) * 10);
            product.setStock(1000 - (page * size + i + 1) * 10);
            products.add(product);
        }
        return products;
    }
}

技术架构

系统架构图

┌─────────────────────┐
│   客户端             │
└──────────┬──────────┘
           │
┌──────────▼──────────┐
│   应用服务           │
│                     │
│ ┌─────────────────┐ │
│ │ 控制器           │ │
│ ├─────────────────┤ │
│ │ 业务逻辑         │ │
│ └─────────────────┘ │
└──────────┬──────────┘
           │
┌──────────▼──────────┐
│   缓存服务           │
│                     │
│ ┌─────────────────┐ │
│ │ 基础缓存操作     │ │
│ ├─────────────────┤ │
│ │ 雪崩防护         │ │
│ └─────────────────┘ │
└──────────┬──────────┘
           │
┌──────────▼──────────┐     ┌─────────────────────┐
│   数据服务           │────▶│   数据库             │
└─────────────────────┘     └─────────────────────┘

工作流程图

  1. 请求处理:客户端发送请求到应用服务
  2. 缓存查询:应用服务查询缓存
  3. 缓存命中:如果缓存命中,直接返回缓存数据
  4. 缓存未命中:如果缓存未命中,获取互斥锁
  5. 获取锁:只有一个线程能获取锁,其他线程等待
  6. 数据查询:获取锁的线程从数据库查询数据
  7. 更新缓存:查询到数据后,更新缓存并添加随机过期时间
  8. 释放锁:完成缓存更新后释放锁
  9. 返回数据:返回查询到的数据给客户端

配置说明

核心配置

配置项说明默认值优化建议
spring.redis.hostRedis 服务器地址localhost生产环境建议使用集群
spring.redis.portRedis 服务器端口6379根据实际配置调整
spring.redis.passwordRedis 密码-生产环境建议设置密码
spring.redis.databaseRedis 数据库索引0根据实际需求调整
spring.redis.timeoutRedis 连接超时时间3000ms根据网络情况调整
spring.redis.lettuce.pool.max-active连接池最大活跃连接数8根据并发度调整
spring.redis.lettuce.pool.max-idle连接池最大空闲连接数8根据并发度调整
spring.redis.lettuce.pool.min-idle连接池最小空闲连接数0根据并发度调整
spring.redis.lettuce.pool.max-wait连接池最大等待时间-1ms根据网络情况调整

缓存配置

配置项说明默认值优化建议
cache.base-expire基础过期时间(毫秒)1800000根据业务需求调整
cache.random-range随机偏移范围(毫秒)600000根据业务需求调整
cache.lock-expire锁过期时间(毫秒)30000根据数据加载时间调整
cache.lock-retry-times锁获取重试次数3根据并发度调整
cache.lock-retry-interval锁获取重试间隔(毫秒)50根据系统性能调整

最佳实践

1. 缓存键设计

  • 命名规范:使用统一的命名规范,如 业务:模块:id
  • 避免过长:缓存键不宜过长,建议不超过 512 字节
  • 避免特殊字符:避免使用 Redis 保留字符
  • 一致性:确保缓存键与业务逻辑一致

2. 过期时间设置

  • 随机偏移:为所有缓存键添加随机过期时间,避免集中过期
  • 分层缓存:对不同类型的数据设置不同的过期时间
  • 热点数据:对热点数据设置较长的过期时间
  • 冷数据:对冷数据设置较短的过期时间

3. 互斥锁实现

  • 锁过期时间:设置合理的锁过期时间,避免死锁
  • 锁释放:确保在任何情况下都能释放锁
  • 锁重试:实现锁获取失败后的重试机制
  • 锁粒度:根据业务需求调整锁的粒度

4. 异常处理

  • 缓存异常:缓存操作失败时,直接访问数据库
  • 数据库异常:数据库访问失败时,返回错误信息
  • 锁异常:锁操作失败时,直接访问数据库
  • 超时处理:设置合理的超时时间,避免请求长时间阻塞

5. 监控与告警

  • 缓存命中率:监控缓存命中率,及时发现问题
  • 缓存过期:监控缓存过期情况,避免集中过期
  • 缓存大小:监控缓存大小,避免内存溢出
  • 数据库压力:监控数据库压力,及时发现缓存失效

问题排查

常见问题及解决方案

问题原因解决方案
缓存雪崩大量缓存 Key 同时过期使用随机偏移,分散过期时间
缓存击穿热点 Key 过期使用互斥锁,避免同时重建缓存
缓存穿透查询不存在的数据使用布隆过滤器,过滤不存在的数据
锁竞争激烈并发度过高调整锁粒度,使用分段锁
锁超时数据加载时间过长增加锁过期时间,优化数据加载逻辑

排查步骤

  1. 查看监控:检查缓存命中率、数据库压力等指标
  2. 分析日志:查看应用日志,找出缓存操作失败的原因
  3. 检查 Redis:检查 Redis 服务状态,是否有内存不足等问题
  4. 分析代码:检查缓存键设计、过期时间设置等是否合理
  5. 压力测试:通过压力测试模拟高并发场景,找出问题所在

调试工具

  • Redis CLI:使用 Redis 命令行工具查看缓存状态
  • Redis Insight:使用 Redis 可视化工具监控缓存
  • Spring Boot Actuator:查看应用健康状态和缓存统计信息
  • JMeter:使用压测工具模拟高并发场景
  • ELK Stack:分析应用日志,找出问题原因

性能测试

测试环境

  • 硬件配置:8核16G服务器
  • 软件版本:Spring Boot 2.7.15, Redis 7.0, MySQL 8.0
  • 测试工具:JMeter
  • 测试场景:1000并发用户,持续测试10分钟

测试结果

场景配置吞吐量 (QPS)响应时间 (ms)错误率
无缓存直接访问数据库50200010%
普通缓存固定过期时间5002001%
随机偏移添加随机过期时间8001500.5%
互斥重建添加互斥锁9001200.3%
综合优化随机偏移 + 互斥重建10001000.1%

优化效果分析

  1. 普通缓存:相比无缓存,吞吐量提升约900%,响应时间减少约90%
  2. 随机偏移:相比普通缓存,吞吐量提升约60%,响应时间减少约25%
  3. 互斥重建:相比随机偏移,吞吐量提升约12.5%,响应时间减少约20%
  4. 综合优化:相比互斥重建,吞吐量提升约11.1%,响应时间减少约16.7%

代码示例

1. 控制器示例

@RestController
@RequestMapping("/api")
public class CacheController {
    @Autowired
    private CacheService cacheService;
    
    @Autowired
    private DataService dataService;
    
    /**
     * 获取用户信息
     */
    @GetMapping("/user/{id}")
    public User getUser(@PathVariable Long id) {
        String key = "user:" + id;
        return cacheService.get(key, User.class, () -> dataService.getUserById(id));
    }
    
    /**
     * 获取商品列表
     */
    @GetMapping("/products")
    public List<Product> getProducts(@RequestParam int page, @RequestParam int size) {
        String key = "products:" + page + ":" + size;
        return cacheService.get(key, new TypeReference<List<Product>>() {}, () -> dataService.getProducts(page, size));
    }
    
    /**
     * 清除缓存
     */
    @DeleteMapping("/cache/{key}")
    public Map<String, Object> clearCache(@PathVariable String key) {
        cacheService.delete(key);
        Map<String, Object> result = new HashMap<>();
        result.put("success", true);
        result.put("message", "缓存清除成功");
        return result;
    }
}

2. 实体类示例

@Data
public class User {
    private Long id;
    private String name;
    private int age;
    private String email;
}

@Data
public class Product {
    private Long id;
    private String name;
    private double price;
    private int stock;
}

3. SpringContextHolder 工具类

@Component
public class SpringContextHolder implements ApplicationContextAware {
    private static ApplicationContext applicationContext;
    
    @Override
    public void setApplicationContext(ApplicationContext context) throws BeansException {
        applicationContext = context;
    }
    
    public static <T> T getBean(Class<T> clazz) {
        return applicationContext.getBean(clazz);
    }
    
    public static Object getBean(String name) {
        return applicationContext.getBean(name);
    }
}

部署与集成

部署步骤

  1. 准备环境:安装 JDK 11+, Redis 7.0+
  2. 配置 Redis:启动 Redis 服务,配置密码和内存限制
  3. 配置应用:修改 application.yml 配置文件
  4. 构建项目mvn clean package
  5. 启动应用java -jar redis-cache-avalanche-demo-1.0.0.jar

集成到现有系统

  1. 添加依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
  2. 配置 Redis

    • 在 application.yml 中配置 Redis 连接信息
    • 配置缓存过期时间和随机偏移范围
  3. 使用缓存服务

    @Autowired
    private CacheService cacheService;
    
    public User getUser(Long id) {
        String key = "user:" + id;
        return cacheService.get(key, User.class, () -> dataService.getUserById(id));
    }
    
  4. 监控缓存

    • 集成 Spring Boot Actuator
    • 配置缓存监控指标

结论

SpringBoot + Redis 缓存雪崩防护方案为解决缓存雪崩问题提供了一个完整的解决方案。通过随机偏移和互斥重建双保险,可以有效避免大量缓存 Key 同时过期导致的系统压力剧增问题。本方案不仅可以应用于传统的单体应用,也可以应用于分布式微服务架构,为系统的稳定运行提供保障。随着业务量的增长和系统复杂度的提高,缓存雪崩防护的重要性将日益凸显,而这个方案将为开发者和运维人员提供有力的支持。

更多技术文章,欢迎关注公众号:服务端技术精选


标题:SpringBoot + Redis 缓存雪崩防护:大量 Key 同时过期?随机偏移 + 互斥重建双保险
作者:jiangyi
地址:http://www.jiangyi.space/articles/2026/04/26/1777041375353.html
公众号:服务端技术精选
    评论
    0 评论
avatar

取消