通常我们为了保证缓存中的数据与数据库中的数据一致性,会给 Redis 里的数据设置过期时间,当缓存数据过期后,用户访问的数据如果不在缓存里,业务系统需要重新生成缓存,因此就会访问数据库,并将数据更新到 Redis 里,这样后续请求都可以直接命中缓存。
缓存雪崩
如果同一时间大量的缓存数据同时过期或redis服务宕机,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机。
发生缓存雪崩有两个原因
- 大量的缓存数据同时过期
- redis服务宕机
大量的缓存数据同时过期的解决方案
1、给不同的key随机设置过期时间 (TTL)
2、互斥锁
当业务线程在处理用户请求时,如果发现访问的数据不在 Redis 里,就加个互斥锁,保证同一时间内只有一个请求来构建缓存(从数据库读取数据,再将数据更新到 Redis 里),当缓存构建完成后,再释放锁。未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
实现互斥锁的时候,最好设置超时时间,不然第一个请求拿到了锁,然后这个请求发生了某种意外而一直阻塞,一直不释放锁,这时其他请求也一直拿不到锁,整个系统就会出现无响应的现象。
3、双 key 策略
我们对缓存数据可以使用两个 key,一个是主 key,会设置过期时间,一个是备 key,不会设置过期,它们只是 key 不一样,但是 value 值是一样的,相当于给缓存数据做了个副本。
当业务线程访问不到「主 key 」的缓存数据时,就直接返回「备 key 」的缓存数据,然后在更新缓存的时候,同时更新「主 key 」和「备 key 」的数据。
4、多级缓存
jvm本地缓存,nginx缓存等
Redis 故障宕机引发的缓存雪崩的解决方案
1、给缓存业务添加服务熔断或请求降级限流策略 (sentinel);
2、搭建redis集群,提供redis服务的可用性;
缓存击穿
缓存击穿也称(热点key问题),一个被高并发访问且缓存业务重建困难的key突然过期(失效)了,于是全部请求都直接访问数据库,从而导致数据库的压力骤增甚至宕机。
如:tb秒杀活动,wb热榜等
解决方案
1、互斥锁
互斥锁保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。最后给锁也添加过期时间
redis命令
SETNX KEY_NAME VALUE
使用setIfAbsent,springboot2.1版本以上才支持
stringRedisTemplate.opsForValue().setIfAbsent(key, "1",10, TimeUnit.SECONDS);
使用lua脚本
private Boolean tryGetLock1(String key){
/** redisUtil.setIfAbsent 新加的带有超时的setIfAbsent 脚本*/
//KEYS[1] 用来表示在redis 中用作键值的参数占位,
// 主要用來传递在redis 中用作keys值的参数
// ARGV[1] 用来表示在redis 中用作参数的占位,
// 主要用来传递在redis中用做 value值的参数。
String newSetIfAbsentScriptStr = " if 1 == redis.call('setnx',KEYS[1],ARGV[1]) then" +
" return 1;" +
" else" +
" return 0;" +
" end;";
//创建 redis脚本对象
RedisScript<Boolean> newSetIfAbsentScript = new DefaultRedisScript<>(newSetIfAbsentScriptStr,Boolean.class);
List<String> keys = new ArrayList<>();
keys.add(key); // key
Object[] values = {"1"}; // value
// 执行脚本
Boolean res = stringRedisTemplate.execute(newSetIfAbsentScript, keys, values);
System.out.println("result:"+res);
return res;
}
上述的分布式锁存在的问题:1、不可重入;2、不可重试;3、超时释放(如果业务还没执行完,这样就会出问题)
在工作中建议使用redisson的lock、tryLock等
2、逻辑过期
流程图
时序图
逻辑过期是指不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间
逻辑过期代码实现示例
/**
* 逻辑过期解决缓存击穿
*/
private Shop queryWithLogicalExpire(Long id){
String key = CACHE_SHOP_KEY + id;
// 查询缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 缓存命中,先将json反序列化成对象
RedisData data = JSONUtil.toBean(shopJson, RedisData.class);
JSONObject jsonData = (JSONObject) data.getData();
Shop notExpireShop = JSONUtil.toBean(jsonData, Shop.class);
LocalDateTime expireTime = data.getExpireTime();
// 过期时间在当前时间之后,数据未过期,直接返回
if (expireTime.isAfter(LocalDateTime.now())){
return notExpireShop;
}
// 已过期,缓存重建
String lockKey = "lock:shop:"+id;
// //获得互斥锁
// Boolean lock = setIfAbsent(lockKey);
RLock rLock = redissonClient.getLock(lockKey);
boolean lock = rLock.tryLock();
//判断是否成功获得锁
if(lock){
//获取锁成功,开启独立线程,缓存重建
es.submit(()->{
try {
//缓存重建
this.saveShopToRedis(id, 20L);
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放锁
unLock(lockKey);
}
});
}
//锁获取失败,返回旧的商铺信息
return notExpireShop;
}
互斥锁和逻辑过期对比
缓存穿透
缓存穿透是指客户端请求的数据在缓存和数据库中都不存在,于是全部请求都直接访问数据库,从而导致数据库的压力骤增甚至宕机。
存在原因:恶意攻击,故意大量访问某些读取不存在数据的业务
解决方案
- 请求校验
- 缓存空值
- 增加id的复杂度,避免被猜到id的规律
- 做好热点数据的限流
- 加强用户权限的校验
- 布隆过滤器
布隆过滤器校验的结果特点是:
- 若过滤器判断某个元素存在,那么这个元素不一定存在
- 若过滤器判断某个元素不存在,那么这个元素一定不存在
布隆过滤器讲解:https://blog.csdn.net/cssweb_sh/article/details/124284785
布隆过滤器代码实现示例
/**
* 初始化布隆过滤器
*/
@PostConstruct
public void initBloomFilter(){
List<Long> ids = list().stream()
.map(Shop::getId)
.collect(Collectors.toList());
// // 设置布隆过滤器的误判率
// bloomFilter.tryInit(ids.size(),0.01);
// 将所有店铺信息添加到布隆过滤器
bloomFilter.add(ids);
}
/**
* 布隆过滤器解决缓存穿透
*/
public Shop queryWithPassThroughByBf(Long id){
boolean b = bloomFilter.contains(id);
String key = CACHE_SHOP_KEY + id;
if (b){
// 查询缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(shopJson)){
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return shop;
}
// ""空值不是null
if (shopJson != null){
return null;
}
// 查询数据库
Shop shop = getById(id);
if (shop == null){
// 若数据不存在,写入空值来应对缓存穿透
stringRedisTemplate.opsForValue().set(key, "",2, TimeUnit.MINUTES);
return null;
}
// 保存缓存到redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),10, TimeUnit.MINUTES);
return shop;
}
return null;
}