前言
利用缓存来提升应用程序性能固然好,那么在使用缓存的同时会不会带来什么问题呢?
今天咋们聊聊使用缓存的过程中会遇到什么问题,又应当如何应对
- 了解系统使用缓存后带来的问题
- 掌握常见缓存一致性问题的解决方案
- 应对缓存穿透、缓存击穿、缓存雪崩
使用缓存常见的问题
我们先回顾一下使用缓存缓存地处理流程
缓存地使用极大地提升了应用程序的性能和效率,特别是数据查询方面.但同时它也带来了一些问题,其中最要害的就是数据一致性问题
从严格意义上来讲这个问题是无解的,如果对实时性要求比较高的业务场景就不应该使用缓存,反之要使用缓存就得接受它的不一致
除了缓存一致性问题之外还会遇到几个典型的问题:缓存穿透、缓存击穿、缓存雪崩
对于使用缓存常见的问题目前业界也都有比较流行的解决方案
我们并不是要更完美地解决这些问题,也不是要颠覆业界流行的解决方案
我们而是要了解缓存在使用过程中遇到的一些问题和解决思路
缓存一致性
数据实时同步
这种数据同步是增量、主动、强一致性的
-
对数据库数据进行更新的时候(新增、修改、删除) 淘汰缓存
-
读取数据的时候更新缓存,为了避免缓存击穿带来的雪崩问题需要做同步处理
控制一个key只有一个线程去读取数据然后更新缓存,其他线程被阻塞等待
-
设置缓存失效时间,这是一个兜底操作.假设更新缓存失败,这个缓存失效时间一到就会把缓存失效
流程图如下
当然这种情况也只是能保证在线程执行顺序不乱的情况下
如果说clinet2执行太快而clinet1还没执行完,那么缓存的数据还是和数据库不一致的
数据准时同步
这种数据同步是增量、被动、准一致性的
- 对数据进行更新操作后发送一个更新缓存的MQ消息(如果要保证数据不丢失在本地建立一张消息表在发送mq失败后可以重试)
- 缓存更新服务消费mq更新数据消息后读取数据库数据进行相关业务处理
- 缓存更新服务更新业务处理结果到缓存中
流程图如下
核心思想是将更新数据的消息通过mq发送到缓存更新服务中,让缓存更新服务读取数据库数据进行更新缓存
任务调度更新
这种方式是通过调度任务进行定时更新缓存,使用场景如:报表统计数据、对账数据定时更新到缓存等实时性要去不高的场景
实现方式比较简单
- Timer或ScheduledExecutorService
- Spring Task定时任务
- 定时任务框架Quartz或xxl-job
流程图如下
核心思想是通过定时的方式读取数据库数据去更新缓存,这种方式会有一定的延迟,要求实时性不那么高场景的可以考虑
binlog日志订阅
通过订阅binlog来更新缓存,把我们搭建的消费服务,作为mysql的一个slave,订阅binlog解析出更新内容,再更新到缓存
流程图如下
它的流程和任务调度更新是非常相似,定时任务是定时主动更新而日志订阅是被动更新
这种方式性能比较好,缓存数据的一致性也得到更好的保证
在mysql压力不大时它的延迟比较低,并且和业务完全解耦,也解决了实时性问题
当然缺点也很明显,需要单独搭建一个binlogClient同步服务,并且要引入binlog同步机制,这样成本就会无形增大
缓存穿透
缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询
查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库中去查询
造成缓存穿透.在流量大时,可能导致DB挂掉,要是有人利用不存在的key频繁攻击我们的应用,这就是系统漏洞了
解决方案
- 针对业务场景对请求的参数进行有效性校验,防止非法请求击垮db.如用户id<=0的直接拦截
- 如果db查询不到数据,保存空对象到缓存层,设置较短的失效时间
- 采用布隆过滤器保存缓存过的key,在访问请求到来时可以过滤掉不存在的key,防止这些请求到db层面
缓存击穿
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期)
这时由于并发请求比较多,同时读缓存没读到数据
又同时去数据库查询数据,引起数据库压力瞬间增大,造成过大压力
解决方案
- 设置热点数据用不过期,如果缓存数据不设置失效时间的话,就不存在热点key过期造成了大量请求到数据库
- 加互斥锁,当缓存数据失效时,保证只有一个请求能够访问到数据库,并更新缓存,其他线程等待并重试
缓存雪崩
缓存雪崩是指缓存中数据大批量到过期时间而查询数据量巨大引起数据库压力过大甚至down机
和缓存击穿不同的是,缓存击穿指并发查询同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查询数据库
解决方案
- 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生
- 如果缓存数据库是分布式部署,将热点数据均匀分布在不同的缓存数据库中
- 设置热点数据永不过期