正常情况下,查询的数据都存在,如果请求一个不存在的数据,也就是缓存和数据库都查不到这个数据,每次都会去数据库查询,这种查询不存在数据的现象我们称为缓存穿透
1.缓存空值
之所以发生穿透,是因为缓存中没有存储这些数据的key,从而每次都查询数据库
缺点:伪造一堆无用的ID,还是会穿透过去的。而且都需要进行缓存的值会增多,可能导致内存增大。
思考:所以可以把数据库的ID放到一个缓存里面,做成一个白名单之类的。不存在则直接返回。但是缺点就是ID数据可能过大,导致过大的缓存压力。
解决办法:BloomFilter
2.BloomFilter
BloomFilter 类似于一个hbase set 用来判断某个元素(key)是否存在于某个集合中
我们把有数据的key都放到BloomFilter中,每次查询的时候都先去BloomFilter判断,如果没有就直接返回null
布隆过滤器。没有的肯定不存在。存在的不一定存在。适用这里的应用场景
在高并发的情况下,大量的请求同时查询同一个key时,此时这个key正好失效了,就会导致同一时间,这些请求都会去查询数据库,这样的现象我们称为缓存击穿
会造成某一时刻数据库请求量过大
采用分布式锁,只有拿到锁的第一个线程去请求数据库,然后插入缓存,当然每次拿到锁的时候都要去查询一下缓存有没有
分布式锁的应用场景
当某一时刻发生大规模的缓存失效的情况,比如你的缓存服务宕机了、热点数据集中失效问题
## 解决办法
- 采用集群,降低服务宕机的概率
- ehcache本地缓存 + Hystrix限流&降级
ehcache 本地缓存的目的也是考虑在 Redis Cluster 完全不可用的时候,ehcache 本地缓存还能够支撑一阵
使用 Hystrix进行限流 & 降级 ,比如一秒来了5000个请求,我们可以设置假设只能有一秒 2000个请求能通过这个组件,那么其他剩余的 3000 请求就会走限流逻辑
我们在设置缓存的时候,一般会给缓存设置一个失效时间,过了这个时间,缓存就失效了。
对于一些热点的数据来说,当缓存失效以后会存在大量的请求过来,然后打到数据库去,从而可能导致数据库崩溃的情况 解决办法
- 设置不同的失效时间
- 采用缓存击穿的解决办法,加锁
- 永不失效,就是采用定时任务对快要失效的缓存进行更新缓存和失效时间
问题:先修改数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致。
解决思路:先删除缓存,再修改数据库。如果数据库修改失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。
因为读的时候缓存没有,则读数据库中旧数据,然后更新到缓存中。
数据发生了变更,先删除了缓存,然后要去修改数据库。
但是还没来得及修改,一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。
随后数据变更的程序完成了数据库的修改。
更新数据的时候,根据数据的唯一标识,将操作路由之后,发送到一个 jvm 内部队列中。
读取数据的时候,如果发现数据不在缓存中,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也发送同一个 jvm 内部队列中。
此时如果一个读请求过来,读到了空的缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成。
方案 问题 问题出现概率 推荐程度 更新缓存 -> 更新数据库 为了保证数据准确性,数据必须以数据库更新结果为准,所以该方案绝不可行 大 不推荐 更新数据库 -> 更新缓存 并发更新数据库场景下,会将脏数据刷到缓存 并发写场景,概率一般 写请求较多时会出现不一致问题,不推荐使用。 删除缓存 -> 更新数据库 更新数据库之前,若有查询请求,会将脏数据刷到缓存 并发读场景,概率较大 读请求较多时会出现不一致问题,不推荐使用 更新数据库 -> 删除缓存 在更新数据库之前有查询请求,并且缓存失效了,会查询数据库,然后更新缓存。如果在查询数据库和更新缓存之间进行了数据库更新的操作,那么就会把脏数据刷到缓存 并发读场景 & 读操作慢于写操作,概率最小 读操作比写操作更慢的情况较少,相比于其他方式出错的概率小一些。勉强推荐。
采用更新前后双删除缓存策略
先淘汰缓存
再写数据库
休眠1秒,再次淘汰缓存
大家应该评估自己的项目的读数据业务逻辑的耗时。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上即可。
这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
问题及解法:
1、同步删除,吞吐量降低如何处理
将第二次删除作为异步的,提交一个延迟的执行任务
2、解决删除失败的方式:
添加重试机制,例如:将删除失败的key,写入消息队列;但对业务耦合有些严重;
-
缓存key设置失效时间
-
先DB操作,再缓存失效
-
写操作都标记key(美团中间件)强制走主库
-
接入美团中间件监听binlog(美团中间件)变化的数据在进行兜底,再删除缓存
-
先判断是否走主库
-
如果走主库,则使用标记(美团中间件)查主库
-
如果不是,则查看缓存中是否有数据
-
缓存中有数据,则使用缓存数据作为结果
-
如果没有,则查DB数据,再写数据到缓存
每天一更新,直接把缓存直接删掉。因为是夜深人静,上班后大家错峰访问,进行了错峰预热。没有缓存雪崩的场景。
但是如果你是要高峰期,进行缓存的清理,就会有雪崩、击穿的场景。
解决方法:
- 穿透: 因为配置了固定数量的进程,最多也只有固定数量的查询。然后视图缓存也缓存了空值,即使下一次同样的请求过来,也是能命中的。最重要做了反爬虫的限流策略,杜绝穿透的情况。
- 击穿,加锁,分布式锁,只有拿到锁的第一个线程去请求数据库,然后插入缓存,当然每次拿到锁的时候都要去查询一下缓存有没有。类似redash的redis锁的查询限制策略。
- 雪崩:缓存永不失效。主动进行热点数据的缓存的更新。冷数据则进行懒更新的方式。查询后判断时间戳版本,过期也先返回,后续异步进行缓存的刷新。刷新缓存的队列可能会重复?版本号,旧的与最新的版本比较,落后就忽略了。
打点,可以获取到昨天/某个周期范围内比较热点的查询,然后进行提前预热.