缓存穿透
缓存穿透是指访问一个不存在的key(这个key不在缓存层),穿透了缓存层直接打到了DB,如果访问量大的话是有把DB打崩的可能性。
解决方案:
缓存空对象,即使访问的是一个不存在的对象,我们也可以吧访问的key值缓存,value直接设个字符串便可,再次访问的时候判断一下就行了
public String cachePenetrate(@PathVariable("id") String id){ // 查下缓存有没有数据,有直接返回 String cache = stringRedisTemplate.opsForValue().get(id); // 没有缓存 if(StringUtils.isEmpty(cache)){ // 从DB拿 String value = ""; value = db.get(id); // 拿完数据缓存,DB没有数据缓存空串 stringRedisTemplate.opsForValue().set(id,StringUtils.isEmpty(value) ? "" : value); // 设置一个过期时间 stringRedisTemplate.expire(id, 60, TimeUnit.SECONDS); return value; }else{ return cache; } }布隆过滤器
对于恶意攻击,向服务器请求大量不存在的数据造成的缓存穿透,可以用布隆过滤器先做一次过滤,对于不存在的数据布隆过滤器一般都能够过滤掉,不让请求再往后端发送。当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在。
添加:
向布隆过滤器中添加 key 时,会使用多个 hash 函数对 key 进行 hash 算得一个整数索引值然后对位数组长度进行取模运算得到一个位置,每个 hash 函数都会算得一个不同的位置。再把位数组的这几个位置都置为 1 就完成了 add 操作。
查询:
向布隆过滤器询问 key 是否存在时,跟 add 一样,也会把 hash 的几个位置都算出来,看看位数组中这几个位置是否都为 1,只要有一个位为 0,那么说明布隆过滤器中这个key 不存在。如果都是 1,这并不能说明这个 key 就一定存在,只是极有可能存在,因为这些位被置为 1 可能是因为其它的 key 存在所致。如果这个位数组比较稀疏,这个概率就会很大,如果这个位数组比较拥挤,这个概率就会降低。
场景:
这种方法适用于数据命中不高、 数据相对固定、 实时性低(通常是数据集较大) 的应用场景, 代码维护较为复杂, 但是缓存空间占用很少。
import lombok.extern.slf4j.Slf4j; import org.redisson.Redisson; import org.redisson.api.RBloomFilter; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Slf4j @Configuration public class BloomFilter implements InitializingBean { @Autowired private Redisson redisson; @Override public void afterPropertiesSet() throws Exception { RBloomFilter<String> bloomFilter = redisson.getBloomFilter("idList"); // 初始化布隆过滤器:预计元素为100000000L,误差率为3% bloomFilter.tryInit(100000000L,0.03); for (int i = 0; i < 1000000000; i++) { bloomFilter.add(String.valueOf(i)); } } }public String cachePenetrate(@PathVariable("id") String id) { RBloomFilter<String> bloomFilter = redisson.getBloomFilter("idList"); boolean exists = bloomFilter.contains(id); if (!exists) { return ""; } // 查下缓存有没有数据,有直接返回 String cache = stringRedisTemplate.opsForValue().get(id); // 没有缓存 if (StringUtils.isEmpty(cache)) { // 从DB拿 String value = ""; // value = db.get(id); // 拿完数据缓存,DB没有数据缓存空串 stringRedisTemplate.opsForValue().set(id, StringUtils.isEmpty(value) ? "" : value); // 设置一个过期时间(60到120) stringRedisTemplate.expire(id, new Random().nextInt(60) + 60, TimeUnit.SECONDS); return value; } else { return cache; } }
缓存击穿
大批量缓存同一时间失效,请求全部打到DB层,可能会造成数据库瞬间压力过大甚至挂掉,对于这种情况我们在批量增加缓存时最好将这一批数据的缓存过期时间设置为一个时间段内的不同时间。
public String cachePenetrate(@PathVariable("id") String id){
// 查下缓存有没有数据,有直接返回
String cache = stringRedisTemplate.opsForValue().get(id);
// 没有缓存
if(StringUtils.isEmpty(cache)){
// 从DB拿
String value = "";
value = db.get(id);
// 拿完数据缓存,DB没有数据缓存空串
stringRedisTemplate.opsForValue().set(id,StringUtils.isEmpty(value) ? "" : value);
// 设置一个过期时间(60到120)
stringRedisTemplate.expire(id,new Random().nextInt(60) + 60, TimeUnit.SECONDS);
return value;
}else{
return cache;
}
}
缓存雪崩
缓存雪崩是指缓存层扛不住压力崩溃了,流量直接打到了后端数据层。存储层也会级联宕机。
- 保证缓存层服务高可用,用哨兵架构或集群架构。
- 做限流熔断。Sentinel或Hystrix。
热点数据
有时候会有一些热点数据,原本不在缓存的,突然间大批量数据打过来,还没来得及建立缓存,流量就打到了DB,瞬间DB宕机。
我们可以利用互斥锁来解决,此方法只允许一个线程重建缓存, 其他线程等待重建缓存的线程执行完, 重新从缓存获取数据即可。
缓存与数据库双写不一致
不管你是读库更新缓存,读库删除缓存在高并发下都会存在数据不一致问题
- 延时双删:读库删除缓存后过一段时间在删一次,防止别的线程更新掉了缓存。不建议,为了解决一个偶发事件拖慢了整个系统
- canal:利用canal监听MySQL的binLog通过MQ解决。
- 加读写锁:保证顺序,串行化执行。
如果能容忍短暂的数据不一致,可以加过期时间解决。
客户端连接池
客户端建议
Redis使用上应避免多个应用使用同一个Redis,大规模互联网产品建议一个服务对应一个Redis,提高性能和存储。
Redis多库不建议使用,集群默认不支持多库
Redis客户端建议使用带有连接池的客户端。
池化思想:所有的池化思想主要基于2方面,其一方便统一管理,其二不随便创建和销毁连接,连接的创建和销毁都会消耗资源,我使用完连接,丢回池中,后面再拿出来。
常用参数
| 参数 | 默认值 | 描述 |
|---|---|---|
| maxTotal | 8 | 连接池最大连接数 |
| maxIdle | 8 | 空闲最大连接数 |
| minIdle | 0 | 空闲最小连接数 |
| blockWhenExhausted | true | 连接池用尽时是否需要等待,为true配合maxWaitMillis(不建议使用默认值) |
| maxWaitMillis | -1(不超时) | 连接池用尽后调用者最大等待时间 |
| testOnBorrow | false | 连接池使用连接时做一次有效性校验(ping一下通不通)无效连接会被移除,连接多不建议开启 |
| testOnReturn | false | 客户端归还连接时做一次有效性校验(ping一下通不通)无效连接会被移除,连接多不建议开启 |
| jmxEnabled | true | 是否开启jmx监控,可用于监控 |
连接池预热
有时我们服务启动时需要处理大量的Redis请求,为了提高性能可以先连接池预热,将连接池中的连接提升到maxIdle数量
List<Jedis> minIdleJedisList = new ArrayList<Jedis>(jedisPoolConfig.getMinIdle());
for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {
Jedis jedis = null;
try {
jedis = pool.getResource();
minIdleJedisList.add(jedis);
jedis.ping();
} catch (Exception e) {
logger.error(e.getMessage(), e);
} finally {
// 注意,这里不能马上close将连接还回连接池,否则最后连接池里只会建立1个连接
// jedis.close();
}
}
// 统一将预热的连接还回连接池
for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {
Jedis jedis = null;
try {
jedis = minIdleJedisList.get(i);
// 将连接归还回连接池
jedis.close();
} catch (Exception e) {
logger.error(e.getMessage(), e);
} finally {
}
}
Redis清除策略
被动删除
Redis是惰性删除的,key过期了Redis是不会主动删除的(需看策略),是在key被查询一次的时候判断一下有没有过期,过期直接删除。
主动删除
由于惰性删除策略无法保证冷数据被及时删掉,所以redis需要清理掉,主要以下2种策略
- 定期清理:Redis会定期主动淘汰一批已过期的key
- 当前已用内存超过maxmemory限定时(配合maxmemory-policy配置)
8种数据淘汰策略
- volatile-ttl:筛选时,会针对设置了过期时间的键值对,根据过期时间的先后进行删除,越早过期的越先被删除
- volatile-random:筛选时,会针对设置了过期时间的键值对,随机删除
- volatile-lru:筛选时,会针对设置了过期时间的键值对,会使用 LRU 算法筛选设置了过期时间的键值对删除
- volatile-lfu:筛选时,会针对设置了过期时间的键值对,会使用 LFU 算法筛选设置了过期时间的键值对删除
- allkeys-random:从所有键值对中随机选择删除(不管设没设过期时间)
- allkeys-lru:使用 LRU 算法在所有数据中进行筛选删除(不管设没设过期时间)
- allkeys-lfu:使用 LFU 算法在所有数据中进行筛选删除(不管设没设过期时间)
- noeviction:默认,不会剔除任何数据,拒绝所有写入操作并返回客户端错误信息”(error) OOM command not allowed when used memory”,此时Redis只响应读操作
淘汰算法
- LRU 算法(Least Recently Used,最近最少使用):淘汰很久没被访问过的数据,以最近一次访问时间作为参考
- LFU 算法(Least Frequently Used,最不经常使用):淘汰最近一段时间被访问次数最少的数据,以次数作为参考。
推荐使用volatile-lru。如果不设置最大内存,当 Redis 内存超出物理内存限制时,内存的数据会开始和磁盘产生频繁的交换 (swap),会让 Redis 的性能急剧下降。
Redis使用建议
避免使用大key,阻塞Redis
禁用耗时操作,阻塞Redis
hgetall、lrange、smembers、zrange、sinter等并非不能使用,但是需要明确N的值。有遍历的需求可以使用hscan、sscan、zscan代替。
禁止线上使用keys、flushall、flushdb等,通过redis的rename机制禁掉命令,或者使用scan的方式渐进式处理
使用批量操作降低网络开销,比如管道操作等等,但不建议一次传输太大的数据
Redis事务功能尽量不使用,可以使用lua脚本解决原子性问题