架构第4章 高性能缓存架构

本文参考《极客时间》- 从0开始学架构

为什么需要缓存

复杂业务场景下,单纯依靠存储系统的性能提升不够的。
典型场景:
1)需要经过复杂运算后得出的数据,存储系统无能为力。
例如,一个论坛需要在首页展示当前有多少用户同时在线,如果使用MySQL来存储当前用户状态,则每次获取这个总数都要“count(*)”大量数据,这样的操作无论怎么优化MySQL,性能都不会太高。如果要实时展示用户同时在线数,则MySQL性能无法支撑。

2)读多写少的数据,存储系统有心无力
绝大部分在线业务都是读多写少。例如,微博、淘宝、微信这类互联网业务,读业务占了整体业务量的90%以上。
以微博为例:一个明星发一条微博,可能几千万人来浏览。如果使用MySQL来存储微博,用户写微博只有一条insert语句,但每个用户浏览时都要select一次,即使有索引,几千万条select语句对MySQL数据库的压力也会非常大。

  • 缓存就是为了弥补存储系统在这些复杂业务场景下的不足。
    基本原理是将可能重复使用的数据放到内存中,一次生成、多次使用,避免每次使用都去访问存储系统。
    缓存能够带来性能的大幅提升,以Memcache为例,单台Memcache服务器简单的key-value查询能够达到TPS 50000以上,
    基本的架构是:
image.png

缓存的架构设计要点

缓存穿透

缓存穿透是指缓存没有发挥作用。查询缓存时,缓存中没有数据。需要去存储系统查询。
有两种情况:
1.存储数据不存在,被访问的数据确实不存在。缓存此时没有分担查询存储系统的访问压力
通常,业务上读取不存在的数据的请求量不会太大,但如果出现一些异常情况,例如被黑客攻击,故意大量访问某些读取不存在数据的业务,有可能会将存储系统拖垮。

  • 解决办法(较简单):
    如果查询存储系统的数据没有找到,则直接设置一个默认值(可以是空值,也可以是具体的值)存到缓存中,这样第二次读取缓存时就会获取到默认值,而不会继续访问存储系统。

2.缓存数据生成耗费大量时间或者资源。
存储系统有数据,但生成缓存耗费较长时间或者耗费大量资源。如果刚好在业务访问的时候缓存失效了,那么也会出现缓存没有发挥作用,访问压力全部集中在存储系统上的情况。

  • 典型场景:电商的商品分页
    假设在某电商平台选择“手机”这个类别查看,由于数据巨大,不能把所有数据都缓存起来,只能按照分页来进行缓存,由于难以预测用户到底会访问哪些分页,因此业务上最简单的就是每次点击分页的时候按分页计算和生成缓存。
    通常情况下这样实现是基本满足要求的,但是如果被竞争对手用爬虫来遍历的时候,系统性能就可能出现问题。
  • 具体的场景有:
    分页缓存的有效期设置1天,若设置太久,缓存不能反应真实的数据。
    通常,用户不会从第1页到最后1页全部看完,一般用户访问集中在前10页,因此第10页以后的缓存过期失效的可能性很大。
    竞争对手每周来爬数据,爬虫会将所有分类的所有数据全部遍历,从第1页到最后1页全部都会读取,此时很多分页缓存可能都失效了。 由于很多分页都没有缓存数据,从数据库中生成缓存数据又非常耗费性能(order by limit操作),因此爬虫会将整个数据库全部拖慢。
    这种情况并没有太好的解决方案,因为爬虫会遍历所有的数据,而且什么时候来爬取也是不确定的,可能每天,每周,一月一次,不能为了应对爬虫而将所有数据永久缓存。
  • 通常的应对方案要么就是识别爬虫然后禁止访问。但这可能会影响SEO和推广;
  • 要么就是做好监控,发现问题后及时处理。因为爬虫不是攻击,不会进行暴力破坏,对系统的影响是逐步的,监控发现问题后有时间进行处理。

缓存雪崩

缓存雪崩是指当缓存失效(过期)后引起系统性能急剧下降的情况。

当缓存过期/清除后,业务系统需要重新生成缓存,再次访问存储系统,再次进行运算,这个处理步骤耗时几十毫秒甚至上百毫秒。
对于一个高并发的业务系统来说,几百毫秒内可能会接到几百上千个请求。
由于旧缓存被清除,新的缓存还未生成,并且处理这些请求线程不知道另外有一个线程正在生成缓存,因此所有的请求都会重新生成缓存,去访问存储系统,从而对存储系统造成巨大的性能压力。这些压力会拖慢整个系统,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃。

缓存雪崩的常见解决方法有两种:更新锁机制和后台更新机制。

  • 1.更新锁
    对缓存更新操作进行加锁保护,保证只有一个线程能够进行缓存更新,未能获取更新锁的线程要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
    对于采用分布式集群的业务系统,由于存在几十上百台服务器,即使单台服务器只有一个线程更新缓存,但几十上百台服务器一起算下来也会有几十上百个线程同时来更新缓存,同样存在雪崩的问题。
    因此分布式集群的业务系统要实现更新锁机制,需要用到分布式锁,如ZooKeeper。

  • 2.后台更新 由后台线程来更新缓存,而不是由业务线程来更新缓存,缓存本身的有效期设置为永久,后台线程定时更新缓存。
    后台定时机制需要考虑一种特殊的场景。当缓存系统内存不够时,会“踢掉”一些缓存数据,从缓存被“踢掉”到下一次定时更新缓存的这段时间内,业务线程读取缓存返回空值,而业务线程本身又不会去更新缓存,因此业务上看到的现象就是数据丢了。
    解决的方式有两种:
    -1)后台线程除了定时更新缓存,还要频繁地去读取缓存(例如,1秒或者100毫秒读取一次),如果发现缓存被“踢了”就立刻更新缓存,这种方式实现简单,但读取时间间隔不能设 置太长,因为如果缓存被踢了,缓存读取间隔时间又太长,这段时间内业务访问都拿不到真正的数据而是一个空的缓存值,用户体验一般。
    -2)业务线程发现缓存失效后,通过消息队列发送一条消息通知后台线程更新缓存。
    可能会出现多个业务线程都发送了缓存更新消息,但其实对后台线程没有影响,后台线程收到消息 后更新缓存前可以判断缓存是否存在,存在就不执行更新操作。
    这种方式实现依赖消息队列,复杂度会高一些,但缓存更新更及时,用户体验更好。

后台更新既适应单机多线程的场景,也适合分布式集群的场景,相比更新锁机制要简单一些。
后台更新机制还适合业务刚上线的时候进行缓存预热。缓存预热指系统上线后,将相关的缓存数据直接加载到缓存系统,而不是等待用户访问才来触发缓存加载。

缓存热点

对于一些特别热点的数据(如电商某秒杀页面/首页banner等)
如果大部分甚至所有的业务请求都命中同一份缓存数据,则这份数据所在的缓存服务器的压力也很大。
例如,赵丽颖微博宣布“结婚,短时间内上千万的用户都会来围观,造成微博短时间挂掉

  • 缓存热点的解决方案就是复制多份缓存副本,将请求分散到多个缓存服务器上,减轻缓存热点导致的单台缓存服务器压力。
    以微博为例,对于粉丝数超过100万的明星,每条微博都可以生成100份缓存,缓存的数据是一样的,通过在缓存的key里面加上编号进行区分,每次读缓存时都随机读取其中某份缓存。
    缓存副本设计有一个细节需要注意,就是不同的缓存副本不要设置统一的过期时间,否则就会出现所有缓存副本同时生成同时失效的情况,从而引发缓存雪崩效应。
    正确的做法是设定一个过期时间范围,不同的缓存副本的过期时间是指定范围内的随机值。

  • 实现方式:
    由于缓存的各种访问策略和存储的访问策略是相关的,因此上面的各种缓存设计方案通常情况下都是集成在存储访问方案中,可以采用“程序代码实现”的中间层方式,也可以采用独立的中间件来实现。

好的缓存方案可以从这几个方面入手设计:
1.什么数据应该缓存
2.什么时机触发缓存和以及触发方式是什么
3.缓存的层次和粒度( 网关缓存如 nginx,本地缓存如单机文件,分布式缓存如redis cluster,进程内缓存如全局变量)
4.缓存的命名规则和失效规则
5.缓存的监控指标和故障应对方案
6.可视化缓存数据如 redis 具体 key 内容和大小

案例:缓存持久化
1.最早采用后台用数据库,前台用关系型数据库+被动缓存的模式。结果经常性能抖动,且缓存一致性问题难解决。
后来我们的多数系统,都采用了前后台分离的模式——后台原始数据仍然是关系型数据库,前台使用缓存作为数据源。
两者之间数据实时同步+定时同步+人工触发结合。
这个模式,基本根除了穿透,雪崩,不一致,性能抖动这些。但带来了新的问题,比如数据丢失且不可恢复。
我们的做法是,让缓存具备相对可靠的持久化机制+运维体系。

2、遇到过几次热点问题,这个更棘手。
第一种情况,单Key数据结构本身过大,单个分片出现热点,单次访问的复杂度变大。
这个相对容易,可以对key进行拆分,使用hashtag机制 分片。
第二种情况,数据分片普遍不均衡,较少遇到,遇到就比较棘手。
第三种情况,数据分片均衡,但访问不均衡,可以增加副本数量。

问题一:商品列表中,像商品描述等信息,缓存更新不及时影响不大,但某些重要数据,如价格,需要及时更新的数据,有没什么好的办法做到刷新。对于价格这种关键数据,不缓存,直接从数据库查询,是否可行。或者在用户查看商品详情时再去数据库查询价格,但可能出现列表中的价格和详情页中的价格不一致。

通常有几种做法:

  1. 同步刷新缓存:当更新了某些信息后,立刻让缓存失效。 这种做法的优点是用户体验好,缺点是修改一个数据可能需要让很多缓存失效
  2. 适当容忍不一致:例如某东的商品就是这样,我查询的时候显示有货,下单的时候提示我没货了
  3. 关键信息不缓存:库存,价格等不缓存,因为这类信息查询简单,效率高,关系数据库查询性能也很高

问题二:数据库自身不是有缓存吗?
以mysql为例子:

  1. mysql第一种缓存叫sql语句结果缓存,但条件比较苛刻,程序员不可控,我们的dba线上都关闭这个功能
  2. mysql第二种缓存是innodb bufer pool,缓存的是磁盘上的分页数据,不是sql的查询结果,sql的执行过程省不了。
    而mc,redis这些实际上都是缓存sql的结果,两种缓存方式,性能差很远。
    因此,可控性,性能是数据库缓存和独立缓存的主要区别。

你可能感兴趣的:(架构第4章 高性能缓存架构)