Redis的缓存穿透、缓存击穿和缓存雪崩
文章目录
- 一、Redis缓存问题概述
- 二、缓存穿透
- 1. 定义
- 2. 解决方案
- 3. 代码示例
- 三、缓存击穿
- 1. 定义
- 2. 解决方案
- 3. 代码示例
- 四、缓存雪崩
- 1. 定义
- 2. 解决方案
- 五、封装Redis工具类
一、Redis缓存问题概述
Redis缓存穿透、击穿和雪崩是缓存机制中常见的问题,具体如下:
- 缓存穿透(Cache Penetration):查询不存在的数据,请求穿过缓存层直达数据库,增加数据库压力。攻击者可构造恶意请求引发此问题。
- 缓存击穿(Cache Breakdown):热点数据失效,大量并发请求直接访问数据库,可能导致数据库崩溃。通常因热点数据过期,同时有大量请求访问该数据。
- 缓存雪崩(Cache Avalanche):大量缓存数据同时失效,大量请求直接访问数据库,造成数据库压力过大。
二、缓存穿透
1. 定义
客户端请求的数据在缓存和数据库中都不存在,缓存永远不生效,请求都打到数据库。
2. 解决方案
- 缓存空对象:请求后发现数据不存在,将null值存入Redis。优点是实现简单、维护方便;缺点是有额外内存消耗,可能造成短期不一致。
- 布隆过滤:在客户端与Redis间加布隆过滤器过滤请求。原理是数据库数据通过hash算法计算hash值存于布隆过滤器,判断数据是否存在时看hash值是0还是1。判断不存在时一定不存在,判断存在时不一定存在,有穿透风险。优点是内存占用少、无多余key;缺点是实现复杂、存在误判可能。
- 其他方案:增强id复杂度、做好数据基础格式校验、加强用户权限校验、做好热点参数限流。
3. 代码示例
@Override
public Result queryById(Long id) {String key = CACHE_SHOP_KEY + id;String shopJson = stringRedisTemplate.opsForValue().get(key);if (StrUtil.isNotBlank(shopJson)) {Shop shop = JSONUtil.toBean(shopJson, Shop.class);return Result.ok(shop);}if ("".equals(shopJson)) {return Result.fail("店铺不存在!");}Shop shop = getById(id);if (shop == null) {stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);return Result.fail("店铺不存在!");}stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);return Result.ok(shop);
}
三、缓存击穿
1. 定义
热点Key突然失效,大量请求瞬间冲击数据库,也叫热点Key问题。
2. 解决方案
- 互斥锁:只有持有锁的线程能访问数据库,会出现相互等待情况。
- 逻辑过期:不设置TTL,用字段(如expire)表示过期时间,手动删除实现过期。优点是异步构建缓存,缺点是构建缓存前返回脏数据。
3. 代码示例
- 互斥锁
private boolean tryLock(String key) {Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.MINUTES);return BooleanUtil.isTrue(flag);
}private void unLock(String key) {stringRedisTemplate.delete(key);
}public Shop queryWithMutex(Long id) {String key = CACHE_SHOP_KEY + id;String shopJson = stringRedisTemplate.opsForValue().get(key);if (StrUtil.isNotBlank(shopJson)) {return JSONUtil.toBean(shopJson, Shop.class);}if (shopJson != null) {return null;}String lockKey = "lock:shop:" + id;Shop shop = null;try {boolean isLock = tryLock(lockKey);if (!isLock) {Thread.sleep(50);return queryWithMutex(id);}shop = getById(id);if (shop == null) {stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);return null;}stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL,TimeUnit.MINUTES);} catch (InterruptedException ex) {throw new RuntimeException(ex);} finally {unLock(lockKey);}return shop;
}
- 逻辑过期
@Data
public class RedisData {private LocalDateTime expireTime;private Object data;
}public void saveShopRedis(Long id, Long expireSeconds) {Shop shop = getById(id);RedisData redisData = new RedisData();redisData.setData(shop);redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);public Shop queryWithLogicalExpire(Long id) {String key = CACHE_SHOP_KEY + id;String shopJson = stringRedisTemplate.opsForValue().get(key);if (StrUtil.isBlank(shopJson)) {return null;}RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);LocalDateTime expireTime = redisData.getExpireTime();if (expireTime.isAfter(LocalDateTime.now())) {return shop;}String lockKey = LOCK_SHOP_KEY + id;boolean islock = tryLock(lockKey);if (islock) {CACHE_REBUILD_EXECUTOR.submit( () -> {try {saveShopRedis(id,20L);} catch (Exception ex) {throw new RuntimeException(ex);} finally {unLock(lockKey);}});}return shop;
}
四、缓存雪崩
1. 定义
同一时段大量缓存key同时失效或Redis服务宕机,大量请求到达数据库,带来巨大压力。
2. 解决方案
- 给不同的Key的TTL添加随机值。
- 利用Redis集群提高服务的可用性。
- 给缓存业务添加降级限流策略。
- 给业务添加多级缓存。
五、封装Redis工具类
@Slf4j
@Component
public class CacheClient {private final StringRedisTemplate stringRedisTemplate;private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);public CacheClient(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}public void set(String key, Object value, Long time, TimeUnit unit) {stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);}public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {RedisData redisData = new RedisData();redisData.setData(value);redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));}public <R,ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){String key = keyPrefix + id;String json = stringRedisTemplate.opsForValue().get(key);if (StrUtil.isNotBlank(json)) {return JSONUtil.toBean(json, type);}if (json != null) {return null;}R r = dbFallback.apply(id);if (r == null) {stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}this.set(key, r, time, unit);return r;}public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {String key = keyPrefix + id;String json = stringRedisTemplate.opsForValue().get(key);if (StrUtil.isBlank(json)) {return null;}RedisData redisData = JSONUtil.toBean(json, RedisData.class);R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);LocalDateTime expireTime = redisData.getExpireTime();if(expireTime.isAfter(LocalDateTime.now())) {return r;}String lockKey = LOCK_SHOP_KEY + id;boolean isLock = tryLock(lockKey);if (isLock){CACHE_REBUILD_EXECUTOR.submit(() -> {try {R newR = dbFallback.apply(id);this.setWithLogicalExpire(key, newR, time, unit);} catch (Exception e) {throw new RuntimeException(e);}finally {unlock(lockKey);}});}return r;}public <R, ID> R queryWithMutex(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {String key = keyPrefix + id;String shopJson = stringRedisTemplate.opsForValue().get(key);if (StrUtil.isNotBlank(shopJson)) {return JSONUtil.toBean(shopJson, type);}if (shopJson != null) {return null;}String lockKey = LOCK_SHOP_KEY + id;R r = null;try {boolean isLock = tryLock(lockKey);if (!isLock) {Thread.sleep(50);return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);}r = dbFallback.apply(id);if (r == null) {stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}this.set(key, r, time, unit);} catch (InterruptedException e) {throw new RuntimeException(e);}finally {unlock(lockKey);}return r;}private boolean tryLock(String key) {Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);return BooleanUtil.isTrue(flag);}private void unlock(String key) {stringRedisTemplate.delete(key);}
}