查询流程:
【问题1】——穿透、击穿及雪崩
问题描述
线程T1在查DB并更新redis缓存的过程中(还未成功将结果放入缓存),有大量并发请求对该Key请求,导致有大量的线程去走查DB并更新redis缓存的流程,不仅对DB造成巨大压力,同时还会产生多次不必要的redis缓存更新操作(网络开销)
问题原因
- 没有对某些可能的热点数据进行预热
- 缓存过期导致【缓存击穿】 或者【缓存雪崩】
- 对同一个不存在的key进行大量请求导致【缓存穿透】
解决方案
- 对于某些可能的热点数据在项目启动的过程中进行预热处理,刷到redis中
- 见下文【缓存击穿】和【缓存雪崩】解决方案
- 见下文【缓存穿透解决方案】
【问题2】——数据延迟、一致性
对于更新操作,更新完DB后需要将缓存中的数据也进行刷新,常见的方式有以下几种
问题描述
- 先删缓存、再更新DB、再放缓存
- 删除缓存后,放入新缓存前可能导致【问题1】,即发生【缓存击穿】。但这种情况仅再超高并发且DB操作较费时的情况下有可能发生,
- 线程1还未更新DB前,线程2访问缓存未命中,去DB拿到旧数据,此时线程1DB更新完成并将新数据放入了缓存中,而线程2这时候将旧数据又放入了缓存中替换了新数据,导致缓存中一直为旧数据
- 先删缓存、再更新DB、不放缓存,下次查询再放缓存
- 如果key为热点key,可能导致【问题1】,即缓存击穿
- 先更新DB、再更新缓存
- 缓存更新前的查询拿到的都是旧数据
- 如果对同一个记录有连续的更新操作,再更新缓存时可能有如1中所示的顺序混乱的情况导致数据不一致
解决方案
- 详见下文缓存击穿解决方案
- 详见下文数据一致性解决方案
解决方案
缓存穿透
描述
- 大量请求某一个不存的key,导致发生【问题1】
- 大量请求不存在的随机key,导致发生【问题1】
解决
- 空值缓存
简单的解决方案,对于某个不存在的key,同样设置到缓存中,值为特定值,过期时间设置的较短,一般为一分钟以内。这种方法仅适用少量key的穿透 - 布隆过滤器
提前对可能的查询条件做布隆过滤器,对所有的请求先过一遍布隆过滤器,过滤掉非法请求 - 其他过滤器
如用户鉴权等对非法请求过滤
缓存击穿
描述
某热点Key失效时,有大量请求进来请求该Key。从而大量请求DB
解决
- 使用锁
当请求缓存未命中时,对后面的请求DB并更新缓存的行为进行加锁,只允许一个线程去做该操作。其他线程在发现被锁住的时候阻塞一会然后重新走查询缓存的流程。 - 逻辑过期
缓存本身不设置过期时间,将过期时间写入到value中,每次查询时判断value是否过期,如果过期,再去更新缓存。但该方案要考虑由于没有过期时间导致LRU替换key的情况,这时候可能导致【缓存击穿】 - 提前更新
类似逻辑过期,但在value中的过期时间要比真正的过期时间要小一些(如统一少三秒),当发现value中的过期时间已经过期时,对这个value过期时间进行修正(再续命两秒)然后走逻辑过期的逻辑;
缓存雪崩
描述
大量key同时过期,导致大量请求进行DB查询,如果某些key时热点key,则这些key都有可能发生【问题1】
解决
key的过期时间要有一定的上下波动性,避免完全一致,分散压力
数据一致性/并发竞争
描述
由于db更新和缓存更新顺序紊乱导致数据不一致的问题
解决
- 将对某数据的db更新和缓存更新操作路由到一个JVM队列中,由一个线程去处理。即将这个操作进行串行化处理。同时队列中如果存在多个缓存更新的任务可以进行合并去重,不需要多次更新。这种解决方案会带来很大性能开销
- 根据版本号进行更新,db操作的数据附带本次操作的版本号(如时间戳等),这个版本号也会存在于缓存的value中,更新缓存时先判断现在缓存中value版本,如果已存在的版本大于目前要更新的版本,则不去进行更新操作。