常见的缓存问题以及解决方案:
当系统处于高峰期的时候,业务流量剧增,给系统带来了很大的压力,严重情况下可能导致系统宕机。
我们一般都会使用 异步/限流/缓存 等措施来减轻DB的 压力。其中每种措施都有复杂的使用场景以及措施。 而其中 缓存我们使用的就有很多,例如缓存 个人主页圈子列表第一页,等一些不太易变的数据。
在使用缓存系统的时候,当遇到大并发的情况下,经常会遇到较多的问题,以下总结一下常见的问题及解决方案。
1. 缓存穿透:
在使用缓存的时候,一般是先去缓存中查询,如果没有值,那么就去数据库取,如果数据库也没有,那么就根据业务需求,抛出异常,或者返回空。
那么就会存在这种情况,如果用户一直在访问一个数据库中不存在的数据,例如 id = -1, 就会导致每次请求都会先去缓存中查询,然后再去数据裤查一次,如果这种请求多的话,就会造成严重的性能问题,这种请求就是缓存穿透。
解决方案:
- 对于请求参数做校验, 用户鉴权,查询条件做 基础逻辑校验,例如 ID > 0, name != “” 等等。 在业务逻辑层 就去规避 数据库层的无效查询。
- 如果在数据库中,根据指定条件没有查询到值,那么可以将对应的字段存储到缓存中去, value 为 null(设置短暂的过期时间,防止后续插入之后仍然查询不到数据),在最近的时间内其他的请求就会直接返回未查到的结果。
2.缓存击穿:
如果一个key 的访问量特别的大,例如 venus 推荐系统的 LoadingCache, 400 QPS, 如果这个 key, 在业务高峰期失效的话,那么就会导致 大量的请求一瞬间 到达数据库,直接挂掉数据库,从而导致所有的上游业务挂掉。
解决方法:
-
对于热点数据,尽量不设置过期时间,如果设置了过期时间,那么要确保 业务高峰期不会过期。
2。 在访问数据库的时候使用互斥锁,当有一个请求在请求数据库的时候,其他请求在外层等待。当请求数据库的线程返回数据,并设置到缓存之后,其他的请求就可以直接取缓存中的数据了。
3.缓存雪崩:
缓存雪崩指的是,在某一时刻,多个大key(多个key)同时失效. 这样就会有大量的请求从缓存中获取不到数据,全部涌向数据库,那么此时就会导致数据库崩溃。
另一种情况,就是缓存服务器(redis) 宕机,也会导致该中情况的产生。
解决方法:
- 对每个key 的过期时间添加上一个 随机值 Math.random() * 1000 ,避免所有的key 的过期时间都相同。
- 使用高可用的分布式 缓存集群,确保缓存的高可用性, redis-cluster
4.双写不一致:
在使用缓存的时候,读和写的流程往往是这样的:
- 读的时候,先去缓存读,如果读不到,再去数据库查,查询到之后放入缓存。
- 写的时候,先失效缓存,然后在进行更新数据库。
所谓双写不一致,就是发生在写操作的时候或者更新的时候,可能会存在缓存中的值和数据库中的值不一致的问题。
更新的时候,要先失效缓存,在更新数据库。为什么先失效缓存呢,就是因为如果先更新数据库,然后在删除缓存的时候失效了,那么就会造成缓存中的值和数据库中的值不一致。
但是这样并不能避免所有的双写不一致的问题。在大并发的情况下,A线程先失效了缓存,再去更新数据库,这时候B线程去读缓存,缓存为空,于是就会去读数据库,然后吧数据库中的旧值设置进入了缓存。
解决方案:
-
比较简单的解决方案 把过期时间设置的较小,这样如果出现了不一致的情况,只有在缓存没过期的时候存在数据不一致的问题,在一些业务场景下可以接受。
-
可以使用辅助队列来解决,先更新数据库,在删除缓存,如果删除失败,则把改次动作放入队列,其他线程从中取出任务不断的尝试去删除该缓存。
-
另一种解决方案是对 一个数据 使用一个队列,使对该 数据的 读写操作串行化。例如 对于ID为N 的数据建立一个队列,对这条数据更新操作,首先删除缓存,然后吧更新操作放入队列,其他的读请求过来之后,查询缓存
如果为空,那么就把该动作加入到队列中去串行执行。 串行化会大大加深程序的复杂性,并极大的降低了程序的吞吐量,可能得不偿失。
一般主流的解决方案就是最简单的 方案一: 先删缓存,然后在对数据库进行更新。
业务 系统使用该种方式也较多。
参考资料:
https://blog.csdn.net/javahongxi/article/details/90114480
https://blog.csdn.net/u010416101/article/details/79690404
TreeMap
https://www.jianshu.com/p/e11fe1760a3d