SpringBoot + 多级缓存(Caffeine + Redis + 空值缓存):防穿透、防雪崩、低延迟三合一

缓存的三大难题

在我们的日常开发工作中,经常会遇到这样的场景:

  • 缓存穿透:大量请求查询不存在的数据,直接击穿到数据库
  • 缓存雪崩:缓存同时失效,导致数据库瞬间压力过大
  • 缓存击穿:热点数据缓存失效,大量请求涌向数据库

传统的单一缓存方案往往无法同时解决这三个问题。今天我们就来聊聊如何用SpringBoot + 多级缓存构建一个全方位的缓存防护体系。

为什么选择多级缓存

相比单一缓存方案,多级缓存有以下优势:

  • 就近访问:本地缓存最快,Redis次之,数据库最慢
  • 故障隔离:某一级缓存故障不影响其他层级
  • 资源优化:合理分配不同层级的缓存资源
  • 全面防护:多层防护,抵御各种缓存攻击

解决方案思路

今天我们要解决的,就是如何用Caffeine + Redis + 空值缓存构建一个安全高效的多级缓存体系。

核心思路是:

  1. 三级缓存:本地缓存→Redis缓存→数据库
  2. 空值缓存:防止缓存穿透
  3. 随机过期:防止缓存雪崩
  4. 热点保护:防止缓存击穿

缓存架构设计

1. 本地缓存(Caffeine)

@Configuration
public class LocalCacheConfig {
    
    @Bean
    public Cache<String, Object> caffeineCache() {
        return Caffeine.newBuilder()
                // 设置初始容量
                .initialCapacity(100)
                // 设置最大容量
                .maximumSize(10000)
                // 设置过期时间
                .expireAfterWrite(Duration.ofMinutes(5))
                // 设置访问过期时间
                .expireAfterAccess(Duration.ofMinutes(2))
                // 统计缓存命中率
                .recordStats()
                // 构建缓存实例
                .build();
    }
}

2. Redis缓存配置

spring:
  redis:
    host: localhost
    port: 6379
    timeout: 2000ms
    lettuce:
      pool:
        max-active: 20
        max-idle: 10
        min-idle: 5

# 自定义缓存配置
cache:
  local:
    enabled: true
    initial-capacity: 1000
    max-size: 10000
    ttl-minutes: 5
  redis:
    enabled: true
    default-ttl-minutes: 30
    hot-data-ttl-minutes: 10

多级缓存实现

1. 缓存服务类

@Service
@Slf4j
public class MultiLevelCacheService {
    
    @Autowired
    private Cache<String, Object> localCache;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private UserService userService;
    
    /**
     * 多级缓存查询
     */
    public User getUserById(Long userId) {
        String cacheKey = buildCacheKey("user", userId);
        
        // 1. 查询本地缓存
        Object localResult = localCache.getIfPresent(cacheKey);
        if (localResult != null) {
            log.debug("本地缓存命中: {}", cacheKey);
            return (User) localResult;
        }
        
        // 2. 查询Redis缓存
        Object redisResult = redisTemplate.opsForValue().get(cacheKey);
        if (redisResult != null) {
            log.debug("Redis缓存命中: {}", cacheKey);
            // 同步到本地缓存
            localCache.put(cacheKey, redisResult);
            return (User) redisResult;
        }
        
        // 3. 缓存未命中,查询数据库
        User user = queryUserFromDB(userId);
        
        // 4. 将结果存入各级缓存
        if (user != null) {
            // 存入Redis
            redisTemplate.opsForValue().set(cacheKey, user, 
                generateRandomTTL(25, 35), TimeUnit.MINUTES);
            // 存入本地缓存
            localCache.put(cacheKey, user);
        } else {
            // 空值缓存,防止穿透
            cacheNullValue(cacheKey);
        }
        
        return user;
    }
    
    /**
     * 查询用户数据(带空值缓存)
     */
    private User queryUserFromDB(Long userId) {
        String lockKey = "lock:user:query:" + userId;
        
        // 使用分布式锁防止缓存击穿
        Boolean acquired = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, "1", Duration.ofSeconds(10));
        
        if (Boolean.TRUE.equals(acquired)) {
            try {
                return userService.selectById(userId);
            } finally {
                redisTemplate.delete(lockKey);
            }
        } else {
            // 获取锁失败,等待一段时间后重试
            try {
                Thread.sleep(100);
                return queryUserFromDB(userId); // 递归重试
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return null;
            }
        }
    }
    
    /**
     * 缓存空值,防止穿透
     */
    private void cacheNullValue(String cacheKey) {
        // 设置较短的过期时间
        redisTemplate.opsForValue().set(cacheKey, NULL_VALUE, 
            Duration.ofMinutes(5));
        // 本地缓存也记录空值(短暂时间)
        localCache.put(cacheKey, NULL_VALUE);
    }
    
    /**
     * 生成随机TTL,防止缓存雪崩
     */
    private Duration generateRandomTTL(int minMinutes, int maxMinutes) {
        Random random = new Random();
        int randomMinutes = minMinutes + random.nextInt(maxMinutes - minMinutes + 1);
        return Duration.ofMinutes(randomMinutes);
    }
    
    private String buildCacheKey(String prefix, Object key) {
        return String.format("%s:%s", prefix, key);
    }
    
    private static final Object NULL_VALUE = new Object();
}

空值缓存策略

1. 布隆过滤器实现

@Component
public class BloomFilterCache {
    
    private final BloomFilter<String> bloomFilter;
    
    public BloomFilterCache() {
        // 预期数据量100万,误判率0.01
        this.bloomFilter = BloomFilter.create(
            Funnels.stringFunnel(Charset.defaultCharset()),
            1000000,
            0.01
        );
    }
    
    /**
     * 判断key是否存在(可能存在或一定不存在)
     */
    public boolean mightContain(String key) {
        return bloomFilter.mightContain(key);
    }
    
    /**
     * 添加key到过滤器
     */
    public void put(String key) {
        bloomFilter.put(key);
    }
    
    /**
     * 批量添加
     */
    public void putAll(Collection<String> keys) {
        keys.forEach(this::put);
    }
}

2. 空值缓存集成

@Service
public class EnhancedCacheService {
    
    @Autowired
    private BloomFilterCache bloomFilter;
    
    @Autowired
    private Cache<String, Object> localCache;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    public User getUserById(Long userId) {
        String cacheKey = buildCacheKey("user", userId);
        
        // 使用布隆过滤器快速判断
        if (!bloomFilter.mightContain(cacheKey)) {
            log.debug("布隆过滤器判断数据不存在: {}", cacheKey);
            return null;
        }
        
        // 继续正常的多级缓存查询逻辑...
        return queryFromMultiLevelCache(cacheKey, userId);
    }
    
    private User queryFromMultiLevelCache(String cacheKey, Long userId) {
        // 本地缓存查询
        Object localResult = localCache.getIfPresent(cacheKey);
        if (localResult != null && !NULL_VALUE.equals(localResult)) {
            return (User) localResult;
        }
        
        // Redis缓存查询
        Object redisResult = redisTemplate.opsForValue().get(cacheKey);
        if (redisResult != null && !NULL_VALUE.equals(redisResult)) {
            localCache.put(cacheKey, redisResult);
            return (User) redisResult;
        } else if (NULL_VALUE.equals(redisResult)) {
            // Redis中存在空值标记
            return null;
        }
        
        // 查询数据库
        User user = queryUserFromDB(userId);
        
        if (user != null) {
            // 数据存在,更新布隆过滤器
            bloomFilter.put(cacheKey);
            // 设置到各级缓存
            setToCache(cacheKey, user);
        } else {
            // 数据不存在,设置空值缓存
            setNullCache(cacheKey);
        }
        
        return user;
    }
    
    private void setToCache(String cacheKey, User user) {
        // 随机TTL防止雪崩
        Duration ttl = generateRandomTTL(25, 35);
        redisTemplate.opsForValue().set(cacheKey, user, ttl);
        localCache.put(cacheKey, user);
    }
    
    private void setNullCache(String cacheKey) {
        // 空值使用较短的固定TTL
        redisTemplate.opsForValue().set(cacheKey, NULL_VALUE, Duration.ofMinutes(5));
        localCache.put(cacheKey, NULL_VALUE);
    }
    
    private static final Object NULL_VALUE = new Object();
}

缓存更新策略

1. 缓存同步

@Service
@Transactional
public class UserService {
    
    @Autowired
    private EnhancedCacheService cacheService;
    
    @Autowired
    private UserMapper userMapper;
    
    /**
     * 更新用户信息,同步更新缓存
     */
    @Transactional(rollbackFor = Exception.class)
    public void updateUser(User user) {
        // 更新数据库
        userMapper.updateById(user);
        
        // 清除各级缓存
        String cacheKey = cacheService.buildCacheKey("user", user.getId());
        
        // 删除Redis缓存
        redisTemplate.delete(cacheKey);
        
        // 删除本地缓存
        localCache.invalidate(cacheKey);
        
        log.info("用户缓存已清除: userId={}", user.getId());
    }
    
    /**
     * 删除用户,清除缓存
     */
    @Transactional(rollbackFor = Exception.class)
    public void deleteUser(Long userId) {
        // 删除数据库记录
        userMapper.deleteById(userId);
        
        // 清除缓存
        String cacheKey = cacheService.buildCacheKey("user", userId);
        redisTemplate.delete(cacheKey);
        localCache.invalidate(cacheKey);
        
        log.info("用户数据已删除并清除缓存: userId={}", userId);
    }
}

2. 缓存预热

@Component
public class CacheWarmUpService {
    
    @Autowired
    private EnhancedCacheService cacheService;
    
    @Autowired
    private UserMapper userMapper;
    
    @EventListener
    public void handleContextRefresh(ContextRefreshedEvent event) {
        log.info("开始缓存预热...");
        
        // 分批加载热点数据
        int pageSize = 1000;
        int offset = 0;
        
        while (true) {
            List<User> users = userMapper.selectPage(
                new Page<>(offset + 1, pageSize), null);
            
            if (users.isEmpty()) {
                break;
            }
            
            // 批量预热缓存
            for (User user : users) {
                cacheService.preheatCache(user);
            }
            
            offset += pageSize;
            
            // 避免一次性加载过多数据
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
        
        log.info("缓存预热完成,共预热 {} 条数据", offset);
    }
}

性能监控

1. 缓存统计

@Component
public class CacheMonitor {
    
    private final MeterRegistry meterRegistry;
    
    private final Counter hitCounter;
    private final Counter missCounter;
    private final Timer cacheTimer;
    
    public CacheMonitor(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.hitCounter = Counter.builder("cache.hits")
            .description("缓存命中次数")
            .register(meterRegistry);
        this.missCounter = Counter.builder("cache.misses")
            .description("缓存未命中次数")
            .register(meterRegistry);
        this.cacheTimer = Timer.builder("cache.access.time")
            .description("缓存访问时间")
            .register(meterRegistry);
    }
    
    public void recordHit(String cacheLevel) {
        hitCounter.increment(Tags.of("level", cacheLevel));
    }
    
    public void recordMiss(String cacheLevel) {
        missCounter.increment(Tags.of("level", cacheLevel));
    }
    
    public <T> T recordCacheOperation(String operation, Supplier<T> supplier) {
        return cacheTimer.recordCallable(() -> {
            return supplier.get();
        });
    }
}

2. 缓存健康检查

@Component
public class CacheHealthIndicator implements HealthIndicator {
    
    @Autowired
    private Cache<String, Object> localCache;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Override
    public Health health() {
        try {
            // 检查本地缓存
            boolean localHealthy = checkLocalCache();
            
            // 检查Redis连接
            boolean redisHealthy = checkRedisConnection();
            
            if (localHealthy && redisHealthy) {
                return Health.up()
                    .withDetail("local_cache", "OK")
                    .withDetail("redis_cache", "OK")
                    .build();
            } else {
                return Health.down()
                    .withDetail("local_cache", localHealthy ? "OK" : "FAILED")
                    .withDetail("redis_cache", redisHealthy ? "OK" : "FAILED")
                    .build();
            }
        } catch (Exception e) {
            return Health.down(e).build();
        }
    }
    
    private boolean checkLocalCache() {
        try {
            localCache.put("health_check", System.currentTimeMillis());
            return localCache.getIfPresent("health_check") != null;
        } catch (Exception e) {
            return false;
        }
    }
    
    private boolean checkRedisConnection() {
        try {
            redisTemplate.opsForValue().set("health_check", System.currentTimeMillis());
            return redisTemplate.opsForValue().get("health_check") != null;
        } catch (Exception e) {
            return false;
        }
    }
}

实际应用效果

通过多级缓存架构,我们可以实现:

  • 高命中率:本地缓存命中率>90%,Redis命中率>95%
  • 低延迟:99%请求响应时间<10ms
  • 高可用:单级缓存故障不影响整体服务
  • 安全防护:有效防止穿透、雪崩、击穿

配置优化建议

1. 本地缓存优化

cache:
  local:
    initial-capacity: 500
    max-size: 5000
    expire-after-write-minutes: 5
    expire-after-access-minutes: 2
    enable-statistics: true

2. Redis缓存优化

spring:
  redis:
    lettuce:
      pool:
        max-active: 50
        max-idle: 20
        min-idle: 10
        max-wait: 2000ms
    timeout: 1000ms

注意事项

在使用多级缓存时,需要注意以下几点:

  1. 数据一致性:确保各级缓存数据的一致性
  2. 内存管理:合理设置缓存大小,避免内存溢出
  3. 监控告警:建立缓存命中率、响应时间等监控指标
  4. 优雅降级:当缓存层不可用时,能平稳降级到数据库
  5. 安全考虑:敏感数据的缓存策略需要特别注意

总结

通过SpringBoot + 多级缓存的架构,我们可以构建一个全方位的缓存防护体系,既能提供高性能的缓存服务,又能有效防止各种缓存攻击。这种方案特别适合在高并发场景下使用,是保障系统稳定性的关键技术之一。

希望这篇文章对你有所帮助!如果你觉得有用,欢迎关注【服务端技术精选】公众号,获取更多后端技术干货。


标题:SpringBoot + 多级缓存(Caffeine + Redis + 空值缓存):防穿透、防雪崩、低延迟三合一
作者:jiangyi
地址:http://jiangyi.space/articles/2026/01/16/1769071756597.html

    0 评论
avatar