中国电网Java面试被问:分布式缓存的缓存穿透解决方案

一、缓存穿透核心问题

1. 问题定义

sql

复制

下载

复制代码
缓存穿透 = 查询不存在的数据 → 缓存不命中 → 直接访问数据库
影响:恶意攻击可导致数据库被压垮

2. 三种缓存异常场景对比

问题 发生条件 影响程度 解决难度
缓存穿透 查询不存在数据 ⭐⭐⭐⭐⭐ ⭐⭐
缓存击穿 热点key过期瞬间 ⭐⭐⭐⭐ ⭐⭐⭐
缓存雪崩 大量key同时过期 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐

二、基础解决方案

1. 空值缓存(基础方案)

java

复制

下载

复制代码
@Component
public class CachePenetrationService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    private static final String NULL_VALUE = "NULL";
    private static final long NULL_CACHE_TTL = 300; // 5分钟
    
    public User getUserById(Long userId) {
        String cacheKey = "user:" + userId;
        
        // 1. 先查缓存
        Object cacheValue = redisTemplate.opsForValue().get(cacheKey);
        
        // 2. 缓存命中
        if (cacheValue != null) {
            // 处理空值标记
            if (NULL_VALUE.equals(cacheValue)) {
                return null; // 缓存了空值,直接返回null
            }
            return (User) cacheValue;
        }
        
        // 3. 缓存未命中,查数据库
        User user = userDao.findById(userId);
        
        // 4. 写入缓存
        if (user != null) {
            redisTemplate.opsForValue().set(
                cacheKey, 
                user, 
                1, TimeUnit.HOURS  // 正常数据TTL
            );
        } else {
            // 空值缓存,防止穿透
            redisTemplate.opsForValue().set(
                cacheKey, 
                NULL_VALUE, 
                NULL_CACHE_TTL, TimeUnit.SECONDS  // 短TTL
            );
        }
        
        return user;
    }
}

2. 布隆过滤器(Bloom Filter)

2.1 基础实现

java

复制

下载

复制代码
@Component
public class BloomFilterService {
    
    // Guava布隆过滤器
    private BloomFilter<String> bloomFilter;
    
    @PostConstruct
    public void init() {
        // 预估元素数量100万,误判率0.01%
        bloomFilter = BloomFilter.create(
            Funnels.stringFunnel(Charset.defaultCharset()),
            1_000_000,
            0.001
        );
        
        // 预热数据
        loadExistingKeys();
    }
    
    public boolean mightExist(String key) {
        return bloomFilter.mightContain(key);
    }
    
    public void addKey(String key) {
        bloomFilter.put(key);
    }
    
    // Redis原生布隆过滤器(Redis 4.0+)
    public boolean redisBloomFilterCheck(Long userId) {
        String key = "user_bloom";
        String item = "user:" + userId;
        
        // BF.EXISTS 检查是否存在
        // BF.ADD 添加元素
        // BF.MADD 批量添加
        // BF.RESERVE 初始化布隆过滤器
        
        return redisTemplate.execute(
            (RedisCallback<Boolean>) connection -> 
                connection.execute("BF.EXISTS", key.getBytes(), item.getBytes())
        );
    }
}
2.2 分布式布隆过滤器

java

复制

下载

复制代码
@Component
public class DistributedBloomFilter {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    private static final String BLOOM_KEY = "global:bloom:user";
    
    /**
     * 使用Redis位图实现分布式布隆过滤器
     */
    public boolean mightExist(Long userId) {
        // 1. 多个哈希函数计算位位置
        int[] positions = hashPositions(userId);
        
        // 2. 检查所有位是否为1
        List<Object> results = redisTemplate.executePipelined(
            (RedisCallback<Object>) connection -> {
                for (int pos : positions) {
                    connection.getBit(BLOOM_KEY.getBytes(), pos);
                }
                return null;
            }
        );
        
        // 3. 所有位都为1才返回true(可能有误判)
        return results.stream().allMatch(result -> 
            Boolean.TRUE.equals(result)
        );
    }
    
    public void add(Long userId) {
        int[] positions = hashPositions(userId);
        
        redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
            for (int pos : positions) {
                connection.setBit(BLOOM_KEY.getBytes(), pos, true);
            }
            return null;
        });
    }
    
    private int[] hashPositions(Long userId) {
        // 使用多个哈希函数
        String key = "user:" + userId;
        return new int[] {
            Math.abs(key.hashCode()) % 1000000,
            Math.abs(murmur3Hash(key)) % 1000000,
            Math.abs(fnv1aHash(key)) % 1000000
        };
    }
}

三、高级防护方案

1. 限流与熔断

1.1 请求限流

java

复制

下载

复制代码
@Component
public class RateLimiterService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    // 滑动窗口限流
    public boolean allowRequest(String key, int maxRequests, long windowSeconds) {
        long currentTime = System.currentTimeMillis();
        long windowStart = currentTime - (windowSeconds * 1000);
        
        String redisKey = "rate_limit:" + key;
        
        // 使用ZSET记录请求时间戳
        redisTemplate.opsForZSet().removeRangeByScore(
            redisKey, 0, windowStart
        );
        
        // 获取当前窗口请求数
        Long count = redisTemplate.opsForZSet().zCard(redisKey);
        
        if (count < maxRequests) {
            // 允许请求,记录时间戳
            redisTemplate.opsForZSet().add(
                redisKey, 
                String.valueOf(currentTime), 
                currentTime
            );
            redisTemplate.expire(redisKey, windowSeconds + 1, TimeUnit.SECONDS);
            return true;
        }
        
        return false;
    }
    
    // 针对不存在key的请求特殊限流
    public boolean allowNullKeyRequest(String keyPrefix, Long id) {
        String key = keyPrefix + ":null:" + id;
        return allowRequest(key, 10, 60); // 60秒最多10次
    }
}

篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc

需要全套面试笔记及答案
【点击此处即可/免费获取】

1.2 熔断器模式

java

复制

下载

复制代码
@Component
public class CircuitBreakerService {
    
    private static final int FAILURE_THRESHOLD = 10;
    private static final long TIMEOUT = 30000; // 30秒
    
    private final AtomicInteger failureCount = new AtomicInteger(0);
    private final AtomicLong lastFailureTime = new AtomicLong(0);
    private volatile State state = State.CLOSED;
    
    enum State { CLOSED, OPEN, HALF_OPEN }
    
    public <T> T execute(Supplier<T> supplier, T fallbackValue) {
        if (state == State.OPEN) {
            // 熔断器打开,检查是否需要进入半开状态
            if (System.currentTimeMillis() - lastFailureTime.get() > TIMEOUT) {
                state = State.HALF_OPEN;
            } else {
                return fallbackValue; // 快速失败
            }
        }
        
        try {
            T result = supplier.get();
            
            if (state == State.HALF_OPEN) {
                // 半开状态成功,关闭熔断器
                state = State.CLOSED;
                failureCount.set(0);
            }
            
            return result;
        } catch (Exception e) {
            handleFailure();
            return fallbackValue;
        }
    }
    
    private void handleFailure() {
        long currentTime = System.currentTimeMillis();
        lastFailureTime.set(currentTime);
        
        if (failureCount.incrementAndGet() >= FAILURE_THRESHOLD) {
            state = State.OPEN;
        }
    }
}

2. 异步更新与预热

2.1 缓存预热

java

复制

下载

复制代码
@Component
public class CacheWarmUpService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    // 启动时预热热点数据
    @EventListener(ApplicationReadyEvent.class)
    public void warmUpCache() {
        List<Long> hotUserIds = userDao.findHotUserIds(1000);
        
        // 批量预热
        Map<String, User> cacheData = new HashMap<>();
        for (Long userId : hotUserIds) {
            cacheData.put("user:" + userId, userDao.findById(userId));
        }
        
        redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
            for (Map.Entry<String, User> entry : cacheData.entrySet()) {
                byte[] key = entry.getKey().getBytes();
                byte[] value = serialize(entry.getValue());
                connection.setEx(key, 3600, value); // 1小时过期
            }
            return null;
        });
    }
    
    // 定时预热
    @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点
    public void scheduledWarmUp() {
        warmUpCache();
    }
}
2.2 异步更新策略

java

复制

下载

复制代码
@Component
public class AsyncUpdateService {
    
    @Autowired
    private ThreadPoolTaskExecutor taskExecutor;
    
    private final Map<String, CompletableFuture<?>> updateTasks = 
        new ConcurrentHashMap<>();
    
    public User getUserAsync(Long userId) {
        String cacheKey = "user:" + userId;
        
        // 1. 先返回缓存旧数据
        User cachedUser = (User) redisTemplate.opsForValue().get(cacheKey);
        
        // 2. 异步更新
        if (cachedUser != null && isNeedUpdate(cachedUser)) {
            String taskKey = "update:" + userId;
            
            if (!updateTasks.containsKey(taskKey)) {
                CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
                    try {
                        User freshUser = userDao.findById(userId);
                        redisTemplate.opsForValue().set(
                            cacheKey, freshUser, 1, TimeUnit.HOURS
                        );
                    } finally {
                        updateTasks.remove(taskKey);
                    }
                }, taskExecutor);
                
                updateTasks.put(taskKey, future);
            }
        }
        
        return cachedUser;
    }
    
    private boolean isNeedUpdate(User user) {
        // 根据业务规则判断是否需要更新
        return System.currentTimeMillis() - user.getUpdateTime().getTime() 
               > 5 * 60 * 1000; // 超过5分钟
    }
}

四、多级缓存架构

1. 本地缓存 + Redis

java

复制

下载

复制代码
@Component
@Slf4j
public class MultiLevelCacheService {
    
    // 本地缓存(Caffeine)
    private final Cache<String, Object> localCache = Caffeine.newBuilder()
        .maximumSize(10000)
        .expireAfterWrite(5, TimeUnit.MINUTES)
        .recordStats()
        .build();
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    public User getUserMultiLevel(Long userId) {
        String cacheKey = "user:" + userId;
        
        // 1. 查本地缓存
        User user = (User) localCache.getIfPresent(cacheKey);
        if (user != null) {
            log.debug("命中本地缓存: {}", userId);
            return user;
        }
        
        // 2. 查Redis
        user = (User) redisTemplate.opsForValue().get(cacheKey);
        if (user != null) {
            // 回填本地缓存
            localCache.put(cacheKey, user);
            log.debug("命中Redis缓存: {}", userId);
            return user;
        }
        
        // 3. 查数据库(带分布式锁防止缓存击穿)
        user = getFromDbWithLock(userId, cacheKey);
        
        return user;
    }
    
    private User getFromDbWithLock(Long userId, String cacheKey) {
        // 分布式锁key
        String lockKey = "lock:" + cacheKey;
        String lockValue = UUID.randomUUID().toString();
        
        try {
            // 尝试获取锁
            boolean locked = redisTemplate.opsForValue()
                .setIfAbsent(lockKey, lockValue, 10, TimeUnit.SECONDS);
            
            if (locked) {
                // 获取到锁,查询数据库
                User user = userDao.findById(userId);
                
                if (user != null) {
                    // 写入多级缓存
                    redisTemplate.opsForValue().set(
                        cacheKey, user, 1, TimeUnit.HOURS
                    );
                    localCache.put(cacheKey, user);
                } else {
                    // 空值缓存
                    redisTemplate.opsForValue().set(
                        cacheKey, "NULL", 5, TimeUnit.MINUTES
                    );
                }
                
                return user;
            } else {
                // 没获取到锁,等待重试
                Thread.sleep(50);
                return (User) redisTemplate.opsForValue().get(cacheKey);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return null;
        } finally {
            // 释放锁(Lua脚本保证原子性)
            String luaScript = 
                "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                "    return redis.call('del', KEYS[1]) " +
                "else " +
                "    return 0 " +
                "end";
            
            redisTemplate.execute(
                new DefaultRedisScript<>(luaScript, Long.class),
                Collections.singletonList(lockKey),
                lockValue
            );
        }
    }
}

2. 热点Key探测与隔离

java

复制

下载

复制代码
@Component
public class HotKeyDetector {
    
    private final ConcurrentHashMap<String, AtomicLong> accessCounter = 
        new ConcurrentHashMap<>();
    
    private final Set<String> hotKeys = ConcurrentHashMap.newKeySet();
    
    @Scheduled(fixedDelay = 10000) // 每10秒检测一次
    public void detectHotKeys() {
        long threshold = 1000; // 10秒内访问超过1000次
        
        accessCounter.forEach((key, counter) -> {
            long count = counter.getAndSet(0);
            
            if (count > threshold) {
                hotKeys.add(key);
                log.info("检测到热点Key: {}, 访问次数: {}", key, count);
                
                // 热点Key特殊处理
                processHotKey(key);
            }
        });
    }
    
    public void recordAccess(String key) {
        accessCounter
            .computeIfAbsent(key, k -> new AtomicLong())
            .incrementAndGet();
    }
    
    private void processHotKey(String key) {
        // 1. 本地缓存热点Key
        // 2. 增加热点Key的副本数
        // 3. 调整过期策略
        // 4. 监控告警
    }
    
    public boolean isHotKey(String key) {
        return hotKeys.contains(key);
    }
}

五、智能防御方案

1. 基于机器学习的异常检测

java

复制

下载

复制代码
@Component
public class AnomalyDetectionService {
    
    // 使用统计学习检测异常访问模式
    private final Map<String, AccessPattern> patternMap = 
        new ConcurrentHashMap<>();
    
    public boolean isMaliciousRequest(String key, Long userId) {
        String patternKey = "pattern:" + key + ":" + userId;
        AccessPattern pattern = patternMap.computeIfAbsent(
            patternKey, k -> new AccessPattern()
        );
        
        pattern.recordAccess();
        
        // 检测异常模式
        return pattern.isAnomalous();
    }
    
    static class AccessPattern {
        private final CircularFifoQueue<Long> accessTimes = 
            new CircularFifoQueue<>(1000);
        private volatile long lastAccessTime = 0;
        
        public void recordAccess() {
            long currentTime = System.currentTimeMillis();
            accessTimes.add(currentTime);
            lastAccessTime = currentTime;
        }
        
        public boolean isAnomalous() {
            if (accessTimes.size() < 10) return false;
            
            // 计算访问频率
            List<Long> times = new ArrayList<>(accessTimes);
            Collections.sort(times);
            
            // 检查是否短时间内高频访问
            long timeWindow = 1000; // 1秒
            int maxAllowed = 50; // 最多50次
            
            int count = 0;
            for (int i = times.size() - 1; i >= 0; i--) {
                if (times.get(times.size() - 1) - times.get(i) <= timeWindow) {
                    count++;
                } else {
                    break;
                }
            }
            
            return count > maxAllowed;
        }
    }
}

2. 动态规则引擎

java

复制

下载

复制代码
@Component
public class DynamicRuleEngine {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    // 规则配置(可从配置中心动态加载)
    private volatile RuleConfig ruleConfig = loadDefaultConfig();
    
    public boolean shouldBlock(String key, String userId, String ip) {
        // 1. 检查IP黑名单
        if (isIpBlacklisted(ip)) {
            return true;
        }
        
        // 2. 检查用户行为
        if (isUserSuspicious(userId)) {
            return true;
        }
        
        // 3. 检查访问模式
        if (isAccessPatternAnomalous(key, userId)) {
            return true;
        }
        
        // 4. 动态规则匹配
        for (Rule rule : ruleConfig.getRules()) {
            if (rule.matches(key, userId, ip)) {
                return true;
            }
        }
        
        return false;
    }
    
    private boolean isIpBlacklisted(String ip) {
        String key = "blacklist:ip:" + ip;
        return Boolean.TRUE.equals(redisTemplate.hasKey(key));
    }
    
    // 动态更新规则
    @Scheduled(fixedRate = 60000)
    public void refreshRules() {
        RuleConfig newConfig = loadConfigFromRemote();
        if (newConfig != null) {
            ruleConfig = newConfig;
        }
    }
}

六、完整防御架构

1. 多层防御架构

java

复制

下载

复制代码
@Component
public class CompleteDefenseService {
    
    @Autowired
    private BloomFilterService bloomFilter;
    
    @Autowired
    private RateLimiterService rateLimiter;
    
    @Autowired
    private CircuitBreakerService circuitBreaker;
    
    @Autowired
    private AnomalyDetectionService anomalyDetection;
    
    public User getUserWithFullProtection(Long userId, String clientIp) {
        String key = "user:" + userId;
        
        // 第一层:基础校验
        if (!validateRequest(userId, clientIp)) {
            return null;
        }
        
        // 第二层:布隆过滤器
        if (!bloomFilter.mightExist(key)) {
            log.warn("布隆过滤器拦截: {}", userId);
            return null;
        }
        
        // 第三层:限流
        if (!rateLimiter.allowRequest("user_query", 100, 60)) {
            log.warn("限流拦截: {}", userId);
            return null;
        }
        
        // 第四层:熔断器
        return circuitBreaker.execute(
            () -> getFromCacheOrDB(userId),
            null
        );
    }
    
    private boolean validateRequest(Long userId, String clientIp) {
        // 1. 参数校验
        if (userId == null || userId <= 0) {
            return false;
        }
        
        // 2. IP校验
        if (anomalyDetection.isMaliciousRequest("user", userId)) {
            return false;
        }
        
        // 3. 频率校验
        String freqKey = "freq:user:" + userId;
        if (!rateLimiter.allowRequest(freqKey, 10, 60)) {
            return false;
        }
        
        return true;
    }
    
    private User getFromCacheOrDB(Long userId) {
        // 带空值缓存的查询逻辑
        // ...
        return null;
    }
}

篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc

需要全套面试笔记及答案
【点击此处即可/免费获取】

2. 监控与告警

java

复制

下载

复制代码
@Component
@Slf4j
public class CachePenetrationMonitor {
    
    private final MeterRegistry meterRegistry;
    
    // 监控指标
    private final Counter penetrationCounter;
    private final Timer queryTimer;
    
    public CachePenetrationMonitor(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        
        this.penetrationCounter = Counter.builder("cache.penetration.count")
            .tag("type", "null_query")
            .register(meterRegistry);
        
        this.queryTimer = Timer.builder("cache.query.duration")
            .register(meterRegistry);
    }
    
    public User getUserWithMonitoring(Long userId) {
        return queryTimer.record(() -> {
            try {
                User user = getUser(userId);
                
                if (user == null) {
                    // 记录穿透
                    penetrationCounter.increment();
                    
                    // 告警逻辑
                    if (penetrationCounter.count() > 1000) {
                        sendAlert();
                    }
                }
                
                return user;
            } catch (Exception e) {
                log.error("查询异常", e);
                return null;
            }
        });
    }
    
    private void sendAlert() {
        // 发送告警通知
        // 1. 企业微信/钉钉
        // 2. 短信
        // 3. 邮件
        log.warn("缓存穿透告警: 超过阈值");
    }
}

七、实战案例与配置

1. Redis配置优化

yaml

复制

下载

复制代码
# application-redis.yml
spring:
  redis:
    lettuce:
      pool:
        max-active: 200
        max-idle: 50
        min-idle: 10
        max-wait: 1000ms
    timeout: 2000ms
    cluster:
      nodes: ${REDIS_NODES}
      max-redirects: 3

# Redisson配置
redisson:
  singleServerConfig:
    address: "redis://127.0.0.1:6379"
    connectionPoolSize: 64
    connectionMinimumIdleSize: 24
    idleConnectionTimeout: 10000
    connectTimeout: 10000
    timeout: 3000
    retryAttempts: 3
    retryInterval: 1500

2. 防护策略配置

yaml

复制

下载

复制代码
# application-defense.yml
cache:
  defense:
    # 布隆过滤器配置
    bloom-filter:
      enabled: true
      expected-insertions: 1000000
      false-probability: 0.001
    
    # 空值缓存配置
    null-cache:
      enabled: true
      ttl: 300s  # 5分钟
      
    # 限流配置
    rate-limit:
      enabled: true
      window-size: 60s
      max-requests: 100
      
    # 熔断器配置
    circuit-breaker:
      enabled: true
      failure-threshold: 10
      timeout: 30s
      
    # 热点Key配置
    hot-key:
      enabled: true
      detection-interval: 10s
      access-threshold: 1000

八、总结与最佳实践

防御策略选择矩阵

场景 推荐方案 原因 实施难度
低频不存在查询 空值缓存 简单有效
海量不存在查询 布隆过滤器 内存效率高 ⭐⭐
恶意攻击防护 限流+熔断 系统保护 ⭐⭐⭐
热点数据查询 多级缓存 性能最优 ⭐⭐⭐⭐
生产环境 组合方案 全面防护 ⭐⭐⭐⭐⭐

最佳实践清单

markdown

复制

下载

复制代码
✅ 必做事项:
  - 所有查询都做参数校验
  - 关键接口必须设置限流
  - 生产环境启用监控告警
  - 定期review缓存命中率

✅ 推荐事项:
  - 使用布隆过滤器过滤非法请求
  - 实现多级缓存架构
  - 热点数据特殊处理
  - 建立熔断降级机制

✅ 高级事项:
  - 实施智能异常检测
  - 建立动态规则引擎
  - 进行定期压测演练
  - 建立故障应急预案

性能与安全性平衡

java

复制

下载

复制代码
// 权衡公式
public class DefenseBalance {
    
    // 防御强度 vs 性能影响
    // 误判率 vs 内存消耗
    // 实时性 vs 准确性
    
    public DefenseStrategy chooseStrategy(
        SecurityRequirement security,
        PerformanceRequirement performance,
        CostConstraint cost
    ) {
        if (security.level == HIGH && performance.requirement == LOW) {
            return DefenseStrategy.AGGRESSIVE;
        } else if (security.level == LOW && performance.requirement == HIGH) {
            return DefenseStrategy.LIGHTWEIGHT;
        } else {
            return DefenseStrategy.BALANCED;
        }
    }
}

核心原则

  1. 防御层层递进:从简单到复杂,从轻量到重量

  2. 监控驱动优化:基于数据做决策,持续改进

  3. 平衡的艺术:在安全、性能、成本间找到最佳平衡点

  4. 持续演进:随着业务发展和攻击手段变化而调整策略

相关推荐
草莓熊Lotso1 天前
脉脉独家【AI创作者xAMA】| 开启智能创作新时代
android·java·开发语言·c++·人工智能·脉脉
爱吃山竹的大肚肚1 天前
Kafka中auto-offset-reset各个选项的作用
java·spring boot·spring·spring cloud
只想要搞钱1 天前
java 常用业务方法-记录
java
CodeAmaz1 天前
HashMap 面试全攻略
java·hashmap
moxiaoran57531 天前
Java设计模式的运用
java·开发语言·设计模式
sheji34161 天前
【开题答辩全过程】以 基于Hadoop教育平台的设计与实现为例,包含答辩的问题和答案
大数据·hadoop·分布式
编程(变成)小辣鸡1 天前
Redisson 知识点及使用场景
java·redisson
源代码•宸1 天前
Leetcode—1339. 分裂二叉树的最大乘积【中等】
开发语言·后端·算法·leetcode·golang·dfs
Chasing Aurora1 天前
C++后端开发之旅(一)
java·开发语言·c++