Redis缓存穿透与解决方案 Redis 的五种数据结构各有特色,用对了才能发挥它的优势。很多人只用到了 String 和 Hash,却不知道 List、Set、ZSet 在特定场景下更合适。本文从应用场景出发,讲什么时候用什么类型。
一、什么是缓存穿透 #
1.1 穿透场景 正常查询: 客户端 → 查询缓存(命中)→ 返回数据 缓存未命中: 客户端 → 查询缓存(未命中)→ 查询数据库(命中)→ 写入缓存 → 返回数据 缓存穿透: 客户端 → 查询缓存(未命中,数据不存在)→ 查询数据库(未命中)→ 返回空 │ └── 每次请求都查数据库!
#
1.2 产生原因
恶意攻击 :构造大量不存在的key进行查询
业务误用 :查询已被删除的数据
数据未初始化 :新数据还未写入缓存和数据库
参数错误 :传入非法参数查询
#
1.3 危害
数据库压力剧增
可能导致数据库宕机
缓存失去保护作用
影响正常用户访问
二、解决方案 #
2.1 方案一:缓存空值 原理 :查询数据库未命中后,将空值也缓存起来
@Service public class UserService { @Autowired private StringRedisTemplate redis; @Autowired private UserMapper userMapper; private static final String USER_KEY = "user:" ; private static final long CACHE_NULL_TTL = 60 ; private static final String NULL_VALUE = "null" ; public User getUser (Long id) { String key = USER_KEY + id; String json = redis.opsForValue().get(key); if (json != null ) { if (NULL_VALUE.equals(json)) { return null ; } return JSON.parseObject(json, User.class); } User user = userMapper.findById(id); if (user == null ) { redis.opsForValue().set(key, NULL_VALUE, CACHE_NULL_TTL, TimeUnit.SECONDS); } else { redis.opsForValue().set(key, JSON.toJSONString(user), 3600 , TimeUnit.SECONDS); } return user; } }
优点 :
缺点 :
缓存大量空值占用空间
如果攻击者构造大量不同的key,缓存空间可能被撑满
数据真实存在后,需要等待空值过期才能正确获取
#
2.2 方案二:布隆过滤器 原理 :在查询缓存之前,先用布隆过滤器判断key是否可能存在
@Configuration public class BloomFilterConfig { @Bean public RBloomFilter<String> userBloomFilter (RedissonClient redisson) { RBloomFilter<String> bloomFilter = redisson.getBloomFilter("userBloomFilter" ); bloomFilter.tryInit(1000000L , 0.01 ); return bloomFilter; } } @Service public class UserService { @Autowired private RBloomFilter<String> userBloomFilter; @Autowired private StringRedisTemplate redis; @Autowired private UserMapper userMapper; @PostConstruct public void initBloomFilter () { List<Long> userIds = userMapper.findAllIds(); for (Long id : userIds) { userBloomFilter.add("user:" + id); } } public User getUser (Long id) { String key = "user:" + id; if (!userBloomFilter.contains(key)) { return null ; } String json = redis.opsForValue().get(key); if (json != null ) { return JSON.parseObject(json, User.class); } User user = userMapper.findById(id); if (user != null ) { redis.opsForValue().set(key, JSON.toJSONString(user), 3600 , TimeUnit.SECONDS); } return user; } public void addUser (User user) { userMapper.insert(user); userBloomFilter.add("user:" + user.getId()); redis.opsForValue().set("user:" + user.getId(), JSON.toJSONString(user), 3600 , TimeUnit.SECONDS); } }
优点 :
缺点 :
有一定误判率(可能将不存在的判断为存在)
不支持删除元素(或需要特殊实现如Counting Bloom Filter)
需要初始化时加载所有数据
#
2.3 方案三:参数校验 @RestController public class UserController { @Autowired private UserService userService; @GetMapping("/user/{id}") public User getUser (@PathVariable Long id) { if (id == null || id <= 0 || id > 999999999L ) { throw new IllegalArgumentException ("Invalid user id" ); } return userService.getUser(id); } }
优点 :
缺点 :
#
2.4 方案四:限流 @Component public class CachePenetrationLimiter { @Autowired private StringRedisTemplate redis; public boolean isAllowed (String key, int maxRequests, int windowSeconds) { String limitKey = "limit:" + key; long now = System.currentTimeMillis(); long windowStart = now - windowSeconds * 1000 ; redis.opsForZSet().removeRangeByScore(limitKey, 0 , windowStart); Long count = redis.opsForZSet().zCard(limitKey); if (count != null && count >= maxRequests) { return false ; } redis.opsForZSet().add(limitKey, String.valueOf(now), now); redis.expire(limitKey, windowSeconds + 1 , TimeUnit.SECONDS); return true ; } } @Service public class UserService { @Autowired private CachePenetrationLimiter limiter; public User getUser (Long id) { if (!limiter.isAllowed("user:" + id, 100 , 60 )) { throw new RateLimitException ("请求过于频繁" ); } } }
#
2.5 方案五:互斥锁(防并发查询) @Service public class UserService { @Autowired private RedissonClient redisson; @Autowired private StringRedisTemplate redis; @Autowired private UserMapper userMapper; public User getUser (Long id) { String key = "user:" + id; String json = redis.opsForValue().get(key); if (json != null ) { return JSON.parseObject(json, User.class); } RLock lock = redisson.getLock("lock:user:" + id); try { if (!lock.tryLock(3 , 10 , TimeUnit.SECONDS)) { Thread.sleep(100 ); return getUser(id); } json = redis.opsForValue().get(key); if (json != null ) { return JSON.parseObject(json, User.class); } User user = userMapper.findById(id); if (user == null ) { redis.opsForValue().set(key, "null" , 60 , TimeUnit.SECONDS); } else { redis.opsForValue().set(key, JSON.toJSONString(user), 3600 , TimeUnit.SECONDS); } return user; } catch (InterruptedException e) { Thread.currentThread().interrupt(); return null ; } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } }
三、方案对比
方案
优点
缺点
适用场景
缓存空值
简单有效
占用空间
数据量可控
布隆过滤器
节省空间
有误判率
大数据量,初始化已知
参数校验
最简单
能力有限
参数范围可控
限流
保护系统
影响正常请求
高并发防御
互斥锁
防并发穿透
复杂度增加
热点key
四、最佳实践 #
4.1 组合方案 @Service public class UserService { @Autowired private RBloomFilter<String> bloomFilter; @Autowired private StringRedisTemplate redis; @Autowired private UserMapper userMapper; @Autowired private RedissonClient redisson; public User getUser (Long id) { if (id == null || id <= 0 ) { return null ; } String key = "user:" + id; if (!bloomFilter.contains(key)) { return null ; } String json = redis.opsForValue().get(key); if (json != null ) { if ("null" .equals(json)) { return null ; } return JSON.parseObject(json, User.class); } RLock lock = redisson.getLock("lock:" + key); try { if (lock.tryLock(3 , 10 , TimeUnit.SECONDS)) { json = redis.opsForValue().get(key); if (json != null ) { return "null" .equals(json) ? null : JSON.parseObject(json, User.class); } User user = userMapper.findById(id); if (user == null ) { redis.opsForValue().set(key, "null" , 60 , TimeUnit.SECONDS); } else { redis.opsForValue().set(key, JSON.toJSONString(user), 3600 , TimeUnit.SECONDS); } return user; } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } return null ; } }
#
4.2 空值缓存的过期策略 @Service public class UserService { private static final long CACHE_NULL_TTL_SHORT = 10 ; private static final long CACHE_NULL_TTL_NORMAL = 60 ; private static final long CACHE_NULL_TTL_LONG = 300 ; public User getUser (Long id) { if (user == null ) { redis.opsForValue().set(key, "null" , CACHE_NULL_TTL_NORMAL, TimeUnit.SECONDS); } } }
五、总结 缓存穿透的防御策略:
参数校验 :第一道防线,拦截明显非法请求
布隆过滤器 :高效判断key是否存在,节省空间
缓存空值 :将不存在的查询结果也缓存
限流 :防止单个key或IP的过度请求
互斥锁 :防止并发查询同一不存在key
核心原则:
在请求到达数据库之前尽可能拦截
使用组合方案增强防御能力
根据业务特点选择合适的策略
持续监控缓存命中率和数据库负载
核心要点
String:简单的键值对,适合缓存、计数器
Hash:存储对象属性,适合用户信息、配置
List:有序列表,适合消息队列、最新列表
Set:无序去重,适合共同好友、抽奖
ZSet:有序集合,适合排行榜、积分系统
总结 选择合适的数据结构是使用 Redis 的关键。在实际项目中,根据业务需求选择合适的类型,可以提升性能和开发效率。