高性能缓存架构
缓存能够带来性能的大幅提升,带来的问题
一、缓存穿透
业务系统虽然去缓存查询数据,但缓存中没有数据,业务系统需要去存储系统查询数据
1. 存储数据不存在
被访问的数据确实不存在。异常情况,比如黑客攻击,故意大量访问某些读取不存在数据的业务
解决:如果查询存储系统的数据没有找到,则直接设置一个默认值(可以是空值,也可以是具体的值)存到缓存中。这样第二次读取时就会获取到默认值,而不会继续访问存储系统
2. 缓存数据生成耗费大量时间或资源
是存储系统中存在数据,但生成缓存数据需要耗费较长时间或者耗费大量资源。如果刚好在业务访问的时候缓存失效了,那么也会出现缓存没有发挥作用,访问压力全部集中在存储系统上的情况。
举一个场景:某个电商平台上“手机”这个类别,由于数据巨大,不能把所有数据都缓存起来,只能分页进行缓存,由于难以预测用户到底会访问哪些分页,因此业务上最简单的就是每次点击分页的时候按分页计算和生成缓存。但是如果被竞争对手用爬虫来遍历,系统性能就会出现问题。
- 分页缓存的有效期设置为 1 天,因为设置太长时间的话,缓存不能反应真实的数据
- 通常情况下,用户不会从第 1 页到最后 1 页全部看完,一般用户访问集中在前 10 页,因此 10 页以后的缓存过期失效的可能性很大
- 爬虫会将所有数据全部遍历,此时很多分页缓存可能都失效了,由于很多分页都没有缓存数据,从数据库中生成缓存数据又非常耗费性能,因此爬虫会将整个数据库全部拖慢
没有太好的解决方案,也不可能为了应对爬虫而将所有数据永久缓存。通常要么就是识别爬虫然后禁止访问;要么就是做好监控,发现问题后及时处理,因为爬虫不是攻击,不会进行暴力破坏,对系统的影响时逐步的,监控发现问题后有时间去处理
二、缓存雪崩
- 缓存过期,生成缓存过程耗时较久,这个过程可能就有大量请求
- 实现缺陷,处理业务请求的线程发现缓存失效都去生成缓存了,从而对存储系统造成巨大的性能压力
解决办法:更新锁机制和后台更新机制
1. 更新锁机制
对缓存更新操作进行加锁保护,保证只有一个线程能够进行缓存更新。未能读取更新锁的线程要么等待锁释放后重新读取缓存,要么就返回空/默认值
对于分布式集群的业务系统,即使单台服务器只有一个线程更新缓存,几百台服务器同时更新,也有几率存在雪崩问题。需要分布式锁,比如 zookeeper
2. 后台更新机制
由后台线程来更新缓存,而不是业务线程来更新,缓存本身的有效期设置为永久,后台线程定时更新缓存
有一种特殊场景:当缓存系统内存不够时,会踢掉一些缓存数据,从缓存被踢掉到下一次定时更新缓存的这段时间内,业务线程读取缓存返回值,而业务线程本身又不会更新缓存,因此业务上看到的现象就是数据丢了。解决:
- 后台线程除了定时更新缓存,还要频繁去读取缓存(1秒或100毫秒读取一次),如果发现缓存被踢了,就立刻更新缓存,但是读取时间间隔不能设置太长。因为这个间隔时间内拿不到真正的数据,用户体验一般
- 业务线程发现缓存失效后,通过消息队列发送一条消息通知后台线程更新缓存。后台线程收到消息后判断缓存是否存在,不存在再更新。会使缓存更新及时,用户体验好
后台更新机制也适用业务刚上线的时候进行缓存预热。系统上线后,即将相关的缓存数据直接加载到缓存系统。
三、缓存热点
如果大部分甚至所有的业务请求都命中同一份缓存数据,则这份数据所在的缓存服务器的压力很大。
解决:复制多份缓存副本,将请求分散到多个缓存服务器上,减轻缓存热点导致的单台缓存服务器压力
注意:不同的缓存副本不要设置统一的过期时间,否则就会出现所有缓存副本同时生成同时失效的情况,从而引发缓存雪崩效应。正确的做法是设定一个过期时间范围,不同的缓存副本的过期时间是指定范围内的随机值
其他问题
- 数据库自身的缓存
- mysql 第一种缓存叫 sql 语句结果缓存,容易失效,且性能低,程序员不可控,一般都是关闭这个功能
- mysql 第二种缓存是 innodb buffer pool,缓存的是磁盘上的分页数据,不是 sql 的查询结果,sql 的执行过程省不了。而 mamcache、redis 都是缓存 sql 的结果