Redis(七)Redis使用上的问题【缓存穿透、缓存失效、缓存雪崩】和优化方案
文章更新时间:2021/08/10
一、缓存穿透
定义:查询一个根本不存在的数据,则缓存层和存储层都不会命中。
弊端:缓存穿透将导致不存在的数据每次请求都要到存储层去查询,失去了缓存保护后端存储的意义。
造成缓存穿透的基本原因有两个:
- 1、自身业务代码或者数据出现问题。
- 2、一些恶意攻击、爬虫等造成大量缓存空命中。
解决方案
缓存空对象
伪代码:
/** * 缓存空对象伪代码 * @param key * @return * @author 有梦想的肥宅 */ public String getKey(String key) { //1、尝试从缓存中获取数据 String cacheValue = cache.get(key); //2、判断缓存是否为空 if (StringUtils.isBlank(cacheValue)) { //3、从数据库中获取 String dbValue = db.get(key); //4、把查询数据库获得的值设置到redis中【查询出null也设置到redis中】 cache.set(key, dbValue); //5、如果存储数据为空,就需要设置一个过期时间(300秒),防止以后要是有真实值了还是一直查询为null if (dbValue == null) { cache.expire(key, 60 * 5); } return dbValue; } else { // 缓存非空 return cacheValue; }
布隆过滤器
定义:布隆过滤器就是一个大型的位数组和几个不一样的无偏hash函数。
PS:无偏就是能够把元素的hash值算得比较均匀。
使用方式:redisson内部对布隆过滤器做了整合,需要使用的时候在工程引入redisson依赖就好
含义:当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在。
解析:
1、布隆过滤器会对key进行一些hash运算,求出几个hash位置,然后会在布隆过滤器里面维护的大型的位数组中,把对应的位置设置为1。
2、那么当我们下次用key来访问时,布隆过滤器也会对其运算,然后查看对应的槽位中的值是不是为1,如果都为1则布隆过滤器返回存在,如果其中1个不为1,则返回key不存在。
PS:从图上我们可以看到为什么布隆过滤器返回key不存在时,是一定不存在的。但是当布隆过滤器返回存在时,key却不一定真实存在,因为可能会有hash碰撞。
PS:虽然这个位数组长度很大,但其实占用的内存很小,因为是位数组,8位才1个字节。
PS:布隆过滤器不能删除数据,如果要删除得重新初始化数据。
PS:布隆过滤器主要是为了在高并发的场景下做一层防御防止缓存击穿的问题,其并不能保证结果是100%准确的。【剩余的漏网之鱼请求下沉到数据库也问题不大了,因为布隆过滤器很强大,已经能帮我们挡掉绝大部分的缓存击穿请求了】
二、缓存失效
定义:由于大批量缓存在同一时间失效可能导致大量请求同时穿透缓存直达数据库,可能会造成数据库瞬间压力过大甚至挂掉。
解决方案
将缓存过期时间设置为一个时间段内的不同时间
伪代码:
/** * 设置一个时间段内的随机时间防止缓存失效造成数据库宕机 * * @param key * @return * @author 有梦想的肥宅 */ public String getKey(String key) { //1、尝试从缓存中获取数据 String cacheValue = cache.get(key); //2、判断缓存是否为空 if (StringUtils.isBlank(cacheValue)) { //3、从数据库中获取 String dbValue = db.get(key); //4、把查询数据库获得的值设置到redis中【查询出null也设置到redis中】 cache.set(key, dbValue); //5、设置一个过期时间(500到1000之间的一个随机数)【这样就可以防止大量缓存同时失效】 int expireTime = new Random().nextInt(500) + 500; //6、如果存储数据为空,就需要设置一个过期时间,防止以后要是有真实值了还是一直查询为null if (dbValue == null) { cache.expire(key, expireTime); } return dbValue; } else { // 缓存非空 return cacheValue; } }
三、缓存雪崩
定义:由缓存雪崩指的是redis集群因为设计或者请求量大等原因挂了,请求会穿透到db层把数据库也打挂掉,从而引起整个系统的瘫痪。
预防和解决
四、热点缓存key重建优化
我们考虑了上面几种情况的解决方案时,redis基本已经可以保护我们的系统了,但是如果出现下面两种情况,可能又会把我们的系统搞挂...
- 一个突然的秒杀活动或者明星效应使得一个平时不怎么访问的商品突然访问量暴增,使其变成热点key
- 重建缓存比较复杂,重建过程耗时比较久。
那么在缓存失效的瞬间,如果有大量线程来重建缓存,会造成后端压力急速增加,甚至可能会让系统崩溃。
解决方案
Redis分布式锁来控制重建缓存的过程
伪代码:
/** * 重建缓存的方法 * * @param key * @return * @author 有梦想的肥宅 */ public String getKey(String key) { //1、尝试从缓存中获取数据 String cacheValue = cache.get(key); //2、判断缓存是否为空,如果value为空,则开始重构缓存 if (StringUtils.isBlank(cacheValue)) { //3、 设置一个分布式锁的key String lockKey = "lock:key:" + key; if (redis.set(lockKey, "1", "1000", "nx")) {//用setnx命令设置分布式锁的key,如果值不存在则返回ture,即获得锁 //4、从数据源获取数据 String dbValue = db.get(key); //5、把查询数据库获得的值设置到redis中【查询出null也设置到redis中】 cache.set(key, dbValue); //6、删除分布式锁的key redis.delete(lockKey); } return dbValue; } else { Thread.sleep(100);//其他线程等待100毫秒后再尝试获取缓存的内容 cacheValue = redis.get(key); return cacheValue; } }
五、缓存与数据库双写不一致
在大并发下,同时操作数据库与缓存会存在数据不一致性问题:
双写不一致
举个🌰:
- 线程1:把商品库存更新为0了【数据库已更新,缓存没更新】
- 线程2:进行补货,把库存更新为3【数据库和缓存均已更新】
- 线程1:开始更新缓存,把缓存更新为0
读写并发不一致
举个🌰:
- 线程1:把商品库存更新为0了,但由于缓存过期或者其他因素使得缓存失效【数据库已更新,但缓存失效】
- 线程3:查询缓存,没有结果,则从数据库获取到库存为0【只查了当前时刻的数据,还没更新缓存】
- 线程2:进行补货,把库存更新为3,但由于缓存过期或者其他因素使得缓存失效【数据库已更新,但缓存失效】
- 线程3:用刚才在数据库中查到的库存0去刷新缓存【用过期数据更新缓存】
解决方案
1、对于并发几率很小的数据(如个人维度的订单数据、用户数据等),这种几乎不用考虑这个问题,很少会发生缓存不一致,可以给缓存数据加上过期时间,每隔一段时间触发读的主动更新即可。
2、就算并发很高,如果业务上能容忍短时间的缓存数据不一致(如商品名称,商品分类菜单等),缓存加上过期时间依然可以解决大部分业务对于缓存的要求。
3、如果不能容忍缓存数据不一致,可以通过加读写锁保证并发读写或写写的时候按顺序排好队,读读的时候相当于无锁。【Redisson对读写锁也做了封装】
4、可以用阿里开源的canal通过监听数据库的binlog日志及时的去修改缓存,但是引入了新的中间件,增加了系统的复杂度。
小结
读多写少:可以加入Redis中间件提高性能。
读多写多且需要保证数据高度一致:直接操作数据库,不建议使用Redis。
PS:放入缓存的数据应该是对实时性、一致性要求不是很高的数据。
PS:不要为了用缓存,同时又要保证绝对的一致性做大量的过度设计和控制,增加系统复杂性!
六、Redis的过期删除策略
策略类型
- 1、被动删除/惰性删除:key过期以后不会立即删除,当下一次访问这个key时,才会删除。
- 2、主动删除:由于惰性删除策略无法保证冷数据被及时删掉,所以Redis会定期主动淘汰一批已过期的key。
- 3、 当前已用内存超过maxmemory限定时,触发主动清理策略。
PS:只有主节点才会执行过期删除策略,主节点删除完了以后会同步del key命令给从节点。
主动删除的几种方式
针对设置了过期时间的key做处理
- 1、volatile-ttl:在筛选时,会针对设置了过期时间的键值对,根据过期时间的先后进行删除,越早过期的越先被删除。
- 2、volatile-random:就像它的名称一样,在设置了过期时间的键值对中,进行随机删除。
- 3、volatile-lru:会使用 LRU 算法筛选设置了过期时间的键值对删除。
- 4、volatile-lfu:会使用 LFU 算法筛选设置了过期时间的键值对删除。
针对所有的key做处理
- 5、allkeys-random:从所有键值对中随机选择并删除数据。
- 6、allkeys-lru:使用 LRU 算法在所有数据中进行筛选删除。
- 7、allkeys-lfu:使用 LFU 算法在所有数据中进行筛选删除。
不处理
- 8、noeviction:不会剔除任何数据,拒绝所有写入操作并返回客户端错误信息"(error) OOM command not allowed when used memory",此时Redis只响应读操作。
两种删除算法
LRU 算法【最近最少使用】
淘汰很久没被访问过的数据,以最近一次访问时间作为参考。
LFU 算法【最不经常使用】
淘汰最近一段时间被访问次数最少的数据,以访问次数作为参考。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律