高性能缓存架构

摘要:
缓存能够带来性能的大幅提升,以Memcache为例,单台Memcache服务器简单的key-value查询能够达到TPS50000以上,其基本的架构是:缓存虽然能够大大减轻存储系统的压力,但同时也给架构引入了更多复杂性。今天,我来逐一分析缓存的架构设计要点。

极客时间:《从 0 开始学架构》:高性能缓存架构

1、引言

前几章节分别从读写分离、分库分表以及数据库的选择等方面来提升系统的性能,但在某些复杂的业务场景下,单纯的提高存储系统的性能是不够的,典型的场景如下:

  • 需要经过复杂运算后得出的数据,存储系统无能为力
  • 读多写少的数据,存储系统有心无力。如写一次,读多次

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

缓存虽然能够大大减轻存储系统的压力,但同时也给架构引入了更多复杂性。架构设计时如果没有针对缓存的复杂性进行处理,某些场景下甚至会导致整个系统崩溃。今天,我来逐一分析缓存的架构设计要点。

2、缓存穿透

缓存穿透是指缓存没有发挥作用,业务系统虽然去缓存查询数据,但缓存中没有数据,业务系统需要再次去存储系统查询数据。通常情况下有两种情况:

  • 1、存储数据不存在
    有可能是真的不存在,也有可能是一些异常情况,如被黑客入侵,故意大量访问某些读取不存在数据的业务,有可能会将存储系统拖垮。

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

  • 2、缓存数据生成耗费大量时间或者资源
    第二种情况是存储系统中存在数据,但生成缓存数据需要耗费较长时间或者耗费大量资源。如果刚好在业务访问的时候缓存失效了,那么也会出现缓存没有发挥作用,访问压力全部集中在存储系统上的情况。
    典型的就是电商的商品分页,某数据量巨大,不能把所有数据都缓存下来,只能按照分页来进行缓存,又由于难以预测用户到底会访问哪些分页,因此业务上最简单的就是每次点击分页的时候按分页计算和生成缓存。通常情况下这样实现是基本满足要求的,但是如果被竞争对手用爬虫来遍历的时候,系统性能就可能出现问题。

3、缓存雪崩

缓存雪崩是指当缓存失效(过期)后引起系统性能急剧下降的情况。当缓存过期被清除后,业务系统需要重新生成缓存,因此需要再次访问存储系统,再次进行运算,这个处理步骤耗时几十毫秒甚至上百毫秒。而对于一个高并发的业务系统来说,几百毫秒内可能会接到几百上千个请求。由于旧的缓存已经被清除,新的缓存还未生成,并且处理这些请求的线程都不知道另外有一个线程正在生成缓存,因此所有的请求都会去重新生成缓存,都会去访问存储系统,从而对存储系统造成巨大的性能压力。这些压力又会拖慢整个系统,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃。
常见的解决方法有两种:更新锁机制后台更新机制
*1、更新锁
对缓存更新操作进行加锁保护,保证只有一个线程能够进行缓存更新,未能获取更新锁的线程要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
对于采用分布式集群的业务系统,由于存在几十上百台服务器,即使单台服务器只有一个线程更新缓存,但几十上百台服务器一起算下来也会有几十上百个线程同时来更新缓存,同样存在雪崩的问题。因此分布式集群的业务系统要实现更新锁机制,需要用到分布式锁,如 ZooKeeper。
*2、后台更新
由后台线程来更新缓存,而不是由业务线程来更新缓存,缓存本身的有效期设置为永久,后台线程定时更新缓存。

后台定时机制需要考虑一种特殊的场景,当缓存系统内存不够时,会“踢掉”一些缓存数据,从缓存被“踢掉”到下一次定时更新缓存的这段时间内,业务线程读取缓存返回空值,而业务线程本身又不会去更新缓存,因此业务上看到的现象就是数据丢了。
解决的方式有两种:

后台线程除了定时更新缓存,还要频繁地去读取缓存(例如,1 秒或者 100 毫秒读取一次),如果发现缓存被“踢了”就立刻更新缓存,这种方式实现简单,但读取时间间隔不能设置太长,因为如果缓存被踢了,缓存读取间隔时间又太长,这段时间内业务访问都拿不到真正的数据而是一个空的缓存值,用户体验一般。

业务线程发现缓存失效后,通过消息队列发送一条消息通知后台线程更新缓存。可能会出现多个业务线程都发送了缓存更新消息,但其实对后台线程没有影响,后台线程收到消息后更新缓存前可以判断缓存是否存在,存在就不执行更新操作。这种方式实现依赖消息队列,复杂度会高一些,但缓存更新更及时,用户体验更好。

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

4、缓存热点

缓存热点的解决方案就是复制多份缓存副本,将请求分散到多个缓存服务器上,减轻缓存热点导致的单台缓存服务器压力。
缓存副本设计有一个细节需要注意,就是不同的缓存副本不要设置统一的过期时间,否则就会出现所有缓存副本同时生成同时失效的情况,从而引发缓存雪崩效应。正确的做法是设定一个过期时间范围,不同的缓存副本的过期时间是指定范围内的随机值。

5、实现方式

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

免责声明:文章转载自《高性能缓存架构》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Oracle数据库的启动和关闭过程Swing之JList的使用下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

java 注解结合 spring aop 实现日志traceId唯一标识

MDC 的必要性 日志框架 日志框架成熟的也比较多: slf4j log4j logback log4j2 我们没有必要重复造轮子,一般是建议和 slf4j 进行整合,便于后期替换为其他框架。 日志的使用 基本上所有的应用都需要打印日志,但并不是每一个开发都会输出日志。 主要有下面的问题: (1)日志太少,出问题时无法定位问题 (2)日志太多,查找问题很麻...

Go语言的调度模型(GPM)

GPM模型 定义于src/runtime/runtime2.go G: Gourtines(携带任务), 每个Goroutine对应一个G结构体,G保存Goroutine的运行堆栈,即并发任务状态。G并非执行体,每个G需要绑定到P才能被调度执行。 P: Processors(分配任务), 对G来说,P相当于CPU核,G只有绑定到P(在P的local ru...

Java 并发系列之七:java 阻塞队列(7个)

1.基本概念 2.实现原理 3.ArrayBlockingQueue 4.LinkedBlockingQueue 5.LinkedBlockingDeque 6.PriorityBlockingQueue 7.DelayQueue 8.SynchronousQueue 9.LinkedTransferQueue 10.小总结 11.t...

多线程中的lua同步问题

最近写paintsnow::start时出现了一个非常麻烦的BUG,程序的Release版本大约每运行十几次就会有一次启动时崩溃(Debug版本还没崩溃过),崩溃点也不固定。经过简单分析之后,确定是线程同步的问题。于是便修改了线程通信的代码,并使用pthread_mutex_lock/unlock来防止冲突。重新编译后,崩溃频率有所减少。但是每运行约四十次...

PHP解决网站大流量与高并发

1:硬件方面   普通的一个p4的服务器每天最多能支持大约10万左右的IP,如果访问量超过10W那么需要专用的服务器才能解决,如果硬件不给力 软件怎么优化都是于事无补的。主要影响服务器的速度 有:网络-硬盘读写速度-内存大小-cpu处理速度。 2:软件方面     第一个要说的就是数据库   首先要有一个很好的架构,查询尽量不用* 避免相关子查询 给经常查...

SendMessage()鼠标软模拟

//鼠标软模拟:好处就是不会真的移动鼠标 开始按钮 坐标 x=386y=387 SendMessage(hookHwnd,messages.WM_LBUTTONDOWN ,0,$0180017A); //按下鼠标左键 SendMessage(hookHwnd,messages.WM_LBUTTONUP ,0, $0180017A); //抬起鼠标左键...