Redis防止缓存穿透的核心目标是避免大量请求直接穿透至数据库,可通过以下四种防护方案实现,需结合业务场景选择或组合使用:
1. 缓存空对象- 原理:当数据库查询结果为空时,将空值(如空字符串或特定标记)缓存至Redis,并设置较短过期时间(如30秒至1分钟)。后续请求可直接从缓存获取空结果,避免重复查询数据库。
- 代码示例:String key = "user:" + userId;String userJson = redisTemplate.opsForValue().get(key);if (StringUtils.isEmpty(userJson)) { User user = userService.getUserById(userId); if (user == null) { redisTemplate.opsForValue().set(key, "", 60, TimeUnit.SECONDS); // 缓存空对象 return null; } else { userJson = JSON.toJSONString(user); redisTemplate.opsForValue().set(key, userJson, 1, TimeUnit.HOURS); }}
- 优点:实现简单,对代码改动小。
- 缺点:占用Redis存储空间,若大量key不存在会浪费资源;数据库插入数据后缓存可能未及时更新。
- 适用场景:缓存数据量小、空结果不频繁的业务。
2. 使用布隆过滤器- 原理:布隆过滤器是一种概率型数据结构,可判断元素“可能存在”或“一定不存在”。预先将所有可能存在的key加载至布隆过滤器,请求时先判断key是否存在,不存在则直接返回,避免查询Redis和数据库。
- 代码示例:String key = "user:" + userId;if (!bloomFilter.mightContain(key)) { return null; // 直接过滤}String userJson = redisTemplate.opsForValue().get(key);
- 优点:性能高,可有效过滤无效请求。
- 缺点:实现复杂,需维护布隆过滤器数据;存在误判率(可能将存在的key误判为不存在);数据库数据变化时需及时更新过滤器。
- 应对误判措施:
设置合理误判率(误判率越低,存储空间需求越大)。
定期从数据库重建布隆过滤器,避免数据不一致。
监控命中率和误判率,动态调整参数或重建过滤器。
- 适用场景:需处理大量无效请求且可容忍一定误判率的业务。
3. 采用互斥锁- 原理:缓存未命中时,通过互斥锁限制仅一个线程查询数据库,其他线程等待,避免大量请求同时穿透至数据库。
- 代码示例:String key = "user:" + userId;String userJson = redisTemplate.opsForValue().get(key);if (StringUtils.isEmpty(userJson)) { RLock lock = redissonClient.getLock("lock:" + key); try { if (lock.tryLock(10, TimeUnit.SECONDS)) { User user = userService.getUserById(userId); if (user == null) { redisTemplate.opsForValue().set(key, "", 60, TimeUnit.SECONDS); return null; } else { userJson = JSON.toJSONString(user); redisTemplate.opsForValue().set(key, userJson, 1, TimeUnit.HOURS); } } else { Thread.sleep(100); // 重试或返回默认值 return getUser(userId); } } finally { lock.unlock(); }}
- 优点:有效避免数据库穿透。
- 缺点:加锁/释放锁增加响应时间;锁过期时间设置不合理可能导致死锁。
- 适用场景:对性能要求较高且需保证数据一致性的业务。
4. 接口层校验参数合法性- 原理:在接口层校验请求参数(如用户ID范围、格式),若参数不合法则直接拒绝请求,避免无效请求穿透至缓存和数据库。
- 代码示例:@GetMapping("/user/{userId}")public User getUser(@PathVariable Long userId) { if (userId == null || userId <= 0 || userId > MAX_USER_ID) { return null; // 或返回错误码 } // 后续缓存和数据库查询逻辑}
- 优点:过滤无效请求,减轻缓存和数据库压力。
- 缺点:增加接口层校验逻辑,提升代码复杂度。
- 适用场景:所有业务场景,建议作为基础防护措施。
方案选择建议- 单一方案选择:
缓存空对象:适用于缓存数据量小、空结果不频繁的场景。
布隆过滤器:适用于需处理大量无效请求且可容忍误判的场景。
互斥锁:适用于对性能要求较高且需保证数据一致性的场景。
接口层校验:适用于所有场景,建议作为基础防护。
- 组合方案:
接口层校验 + 布隆过滤器:过滤无效请求,降低布隆过滤器误判影响。
接口层校验 + 互斥锁:避免无效请求触发锁机制,提升性能。
多层防护:结合接口层校验、布隆过滤器、缓存空对象和互斥锁,构建综合防护体系。
识别缓存穿透的方法- 监控key访问模式:若某不热门key突然被大量访问且未命中率高,可能存在穿透。
- 数据库压力监控:数据库出现大量针对不存在数据的查询,可能是穿透迹象。
- 日志分析:通过应用服务器日志查找针对不存在key的查询请求。
- 监控工具:使用Prometheus和Grafana监控Redis命中率、未命中率及数据库查询量等指标。
总结:Redis缓存穿透需结合业务场景选择防护方案,建议以接口层校验为基础,组合使用布隆过滤器、缓存空对象或互斥锁,并监控布隆过滤器误判率,动态调整参数或重建过滤器,以实现高效防护。