【架构设计】16-高性能缓存

前言

虽然我们可以通过很多方式增加存储系统的性能,但某些场景下,只通过存储系统提升性能是不够的:

  • 需要通过复杂的运算得出结果的数据
  • 读多写少的数据:比如明星微博,只要一次Insert,但是却可能需要几千万次的select,对数据库的压力很大。

缓存就是为了解决这些业务场景。其基本原理是将可能要重复使用的数据放在内存中,一次生成,多次使用,避免多次访问存储系统。

缓存能够带来极大的性能提升。单台的Memcache简单的Key-Value查询可以达到50000TPS。

缓存虽然大大的降低了存储系统的压力,但是也带来了一些设计复杂度。

缓存穿透

缓存穿透是指缓存没有发生作用,业务虽然是去缓存中读取数据,但是缓存中没有数据,还是直接访问了存储系统。通常有二种情况:

  • 存储数据不存在

    这种情况是被访问的数据确实不存在。如果存储系统中某个数据不存在,则不会缓存某个数据。这样在查询的时候,每次都在缓存中找不到数据,然后查询数据库,然后返回数据不存在。缓存在这种情况下并没有起到缓存的作用。

    通常情况下,业务访问不存在数据的访问量不会很大。但如果被黑客攻击的话,故意大量访问不存在的数据,会造成存储系统压力急剧下降。

    解决这个问题的方法就是:如果查询某个数据不存在,则返回一个默认值存到缓存系统中。这样每二次从缓存系统中读取数据就会获取到默认值,而不会再次访问存储系统。

  • 缓存数量生成花费大量的时间或资源

    这种情况是指存储系统中存在数据,但是生成缓存数据需要花费大量的时间或资源。如果刚好在业务访问的时候,缓存已经失效了,也会出现缓存没有发生作用的情况,访问压力就全部集中到存储系统。

    典型的就是电商的商品分页,假设我们在某个电商平台上选择“手机”这个类别查看,由于数据巨大,不能把所有数据都缓存起来,只能按照分页来进行缓存,由于难以预测用户到底会访问哪些分页,因此业务上最简单的就是每次点击分页的时候按分页计算和生成缓存。通常情况下这样实现是基本满足要求的,但是如果被竞争对手用爬虫来遍历的时候,系统性能就可能出现问题。

    具体的场景有:

    • 分页缓存的有效期设置为 1 天,因为设置太长时间的话,缓存不能反应真实的数据。
    • 通常情况下,用户不会从第 1 页到最后 1 页全部看完,一般用户访问集中在前 10 页,因此第 10 页以后的缓存过期失效的可能性很大。
    • 竞争对手每周来爬取数据,爬虫会将所有分类的所有数据全部遍历,从第 1 页到最后 1 页全部都会读取,此时很多分页缓存可能都失效了。
    • 由于很多分页都没有缓存数据,从数据库中生成缓存数据又非常耗费性能(order by limit 操作),因此爬虫会将整个数据库全部拖慢。

      这种情况并没有太好的解决方案,因为爬虫会遍历所有的数据,而且什么时候来爬取也是不确定的,可能是每天都来,也可能是每周,也可能是一个月来一次,我们也不可能为了应对爬虫而将所有数据永久缓存。通常的应对方案要么就是识别爬虫然后禁止访问,但这可能会影响 SEO 和推广;要么就是做好监控,发现问题后及时处理,因为爬虫不是攻击,不会进行暴力破坏,对系统的影响是逐步的,监控发现问题后有时间进行处理。

缓存雪崩

缓存雪崩是指缓存失效后引起系统急剧下降的情况。当缓存过期清除后,业务系统需要重新生成缓存,因此需要再次访问存储系统,重新生成缓存,这个过程需要几十毫秒,或都上百毫秒。对于高并发系统来说,在这段时间内可能会有成百上千的访问。由于旧的缓存已经清除,新的缓存还没有生成,并且对于这些请求来说,还不知道有其它线程在生成缓存,于是也开始生成缓存,访问存储系统,这就会级存储系统带来及大的性能压力。

解决缓存雪崩问题有二种方法:更新锁机制后台更新

  • 更新锁机制

    对缓存更新操作进行加锁操作,保证只有一个线程在更新缓存,未能获取锁的线程要么等待缓存更新完成重新读取缓存,要么直接返回默认值。

    对于分布式系统来说,更新锁需要采用分布式锁,如zookeeper。

  • 后台更新

    由后台线程去更新缓存,而不是由业务更新缓存。缓存失效时间设置成永久,后台线程定时更新缓存。

    后台定时更新需要注意一种情况,就是缓存内存不够的情况下,会“踢掉”一些数据,从缓存被“踢掉”到重新生成,需要一段时间,这段时间业务访问系统会返回空或都默认值,而业务系统又不会去更新缓存,给业务的现象就是数据丢失了。解决这个问题方法有:

    • 后台进程除了定时更新缓存,还要频繁的去读取缓存,如果发现缓存被“踢掉后”立即更新缓存。这种方式实现简单,但是间隔时间不能太久。如果缓存被踢掉,但是间隔时间太长,在这段间隔时间内,如果业务访问缓存,仍然会读取到空数据。用户体验一般。

    • 业务线程发现缓存失效后,通过消息队列发送一条消息通知后台进程更新缓存。可能会出现多个业务请求发送多个消息通知进程更新缓存,后台进行收到消息后,需要判断缓存是否存在,如果存在则直接返回,不存在再做更新。这种方式依赖消息队列,复杂度较高,但缓存更新及时,用户体验好。

      后台更新适应于单机多线程的场景,也适用于分布式集群的场景,比更新锁机制实现简单。

      后台更新也适应新系统上线进行缓存预热,将相关的缓存数据加载到缓存系统,而不是等待用户访问在去加载缓存系统。

缓存热点

虽然缓存系统本身的系统较高,但是对于一些特别的热点数据,如果大部分或全部的业务都命中同一份缓存的话,对于缓存系统服务器的压力也会很大。

缓存热点的解决方案就是对同一份缓存数据复制多个副本,分散到不同的服务器上,减轻缓存热点在单台服务器上的压力。

缓存热点的设计要点在于不要把缓存副本的失效时间设置为同一值,否则就会缓存同时生效或缓存同时失效
导致性能问题,引发缓存雪崩。可以设定一个缓存失效时间范围,不同的缓存副本的失效值是缓存失效时间范围的随机值。

实现方式

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


   转载规则


《【架构设计】16-高性能缓存》 孤独如梦 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
下一篇 
【架构设计】15-高性能NoSQL 【架构设计】15-高性能NoSQL
关系型数据库的缺点关系型数据库已经非常成熟,但是并不是完美的,仍然有以下缺点: 关系型数据库存储的是行记录,无法存储数据结构。 关系型数据库的Schema扩展非常不方便。 关系型数据库的Schema是强约束,无法操作不存在的列。当要扩
2019-05-29
  目录