虽然我们可以通过各种手段来提升存储系统的性能,但是单纯依靠存储系统的性能提升不够的典型场景有:
- 需要经过复杂运算后得出的数据,存储系统无能为力
例如,一个论坛需要在首页展示当前有多少用户同时在线,如果使用mysql来存储当前用户状态,则每次获取这个总数都要count(*)大量数据,这样的操作无论怎么优化mysql,性能都不会太高。如果要实时展示用户同时在线数,则mysql性能无法支撑。
- 读多写少的数据,存储系统有心无力
绝大部分在线业务都是读多写少。例如,微博、淘宝、微信这类互联网业务,读业务占了整体业务量的90%以上。以微博为例:一个明星发一条微博,可能几千万人来浏览。如果使用mysql来存储微博,用户写微博只有一条insert语句,但是每个用户浏览的时候都要select一次,即使有索引,几千万条select语句对于mysql的压力也会很大。
讲道理,缓存就是为了弥补存储系统在这些复杂业务场景下的不足,其基本原理就是将可能重复使用的数据放到内存中,一次生成,多次使用,避免每次使用都去访问存储系统。
缓存是可以带来性能的大幅提升的,以Memcache为例,单台Memcache服务器简单的k-v查询能够到TPS 50000以上,其基本的架构是:
缓存虽然能够大大减轻存储系统的压力,但是同时也会给架构引入更多复杂性。架构设计时如果没有针对缓存的复杂性进行处理,某些场景下甚至会导致整个系统崩溃。
今天就来逐一分析一下架构设计要点:
缓存穿透
缓存穿透是指缓存没有发挥作用。业务系统虽然去缓存查询数据,但是缓存中没有数据,业务系统需要再次去存储系统查询数据。通常情况下有两种情况:
- 存储数据不存在
第一种情况很容易理解,缓存中没有你这个数据,那就跳过缓存查sql,这就叫做一种穿透,只不过一般也就穿透一次,然后就return相关的token放缓存里面了。
- 缓存数据生成耗费大量时间或者资源
如果生成缓存的代价很大,或者时间很长,那么如果访问的时候,这种缓存失效了,也会有缓存穿透的情况。
比如说,你逛淘宝,不可能你看的这个东西,所有的信息都给你放缓存里,那太多了,一般都是你看第一页存第一页,你看第二页存第二页,一般人都是看前几页,所以你看后几页应该缓存失效的就快,不过要是爬虫爬你网站,你没有安防的话,那就比较难受了。
具体的场景有:
- 分页缓存的有效期设置为1天,因为设置时间太长了,这东西更新了你也不知道
- 通常情况下,用户不会从第一页到最后一页都看完,所以缓存主要集中存储前10页
- 爬虫从第一页遍历到最后一页,这样缓存就会存储太多且又慢
- 因为很多缓存是没有的,那么数据库生成缓存又要费时间,因此爬虫就会将整个数据库全部拖慢
以上的情况,没有太好的解决办法,因为我们不知道自己的网站啥时候就被爬了,只能提升网络防御能力了,进行一些混淆加密之类的。
缓存雪崩
缓存雪崩指的是,当缓存失效后引起系统性能急剧下降的情况。意思就是说,缓存失效重新计算生成的时候,这个过程是需要几十毫秒的,在这几十毫秒内,小系统没问题,高并发可能就要承受成千上万个http请求,这些请求到数据,不知道已经有线程去处理生成缓存这个任务了,所以他们就会重新再去开启生成缓存的进程,于是就会导致数据库宕机,这就叫缓存雪崩。
解决方法有两种:更新锁机制和后台更新机制
- 更新锁
对缓存更新操作进行加锁保护,保证只有一个线程进行缓存更新,未能获取更新锁的线程要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。(可以理解成线程通讯)
对于分布式集群的业务系统,因为存在几十上百台服务器,即使单台服务器只有一个线程更新缓存,但是几十上百台服务器一起算下来也会有几十上百个线程同时来更新缓存,同样存在雪崩的问题。因此分布式集群的业务系统要实现更新锁机制,需要用到分布式锁,如zk
- 后台更新
由后台线程来更新缓存,而不是由业务线程来更新缓存,缓存本身的有效期设置为永久,后台线程定时更新缓存。
后台定时机制需要考虑一种特殊的场景,当缓存系统内存不够的时候,会“踢掉一些缓存数据”,从缓存被“踢掉”到下一次定时更新缓存的这段时间,业务线程读取缓存返回空值,而业务线程本身又不会去更新缓存,因此业务上看到的现象就是数据丢了。解决的方式有两种:
-
后台线程除了定时更新缓存,还要频繁地去读取缓存(例如,1秒或者100ms读取一次),如果发现缓存被“踢了”就立刻更新缓存,这种方式实现简单,但是读取时间间隔不能设置太长,因为如果缓存被踢了,缓存读取时间间隔又长,这段时间内业务访问都拿不到一个真正的数据而是一个空的缓存值,体验一般。
-
业务线程发现缓存失效之后,通过消息队列通知后台线程更新缓存,可能会出现多个业务线程都被发送了缓存更新消息,但其实对后台线程没有影响,后台线程收到消息后更新缓存前可以判断缓存是否还存在着。但是依赖消息队列,会让复杂性提高,但是如果缓存更新及时,用户体验就会好很多。
后台更新既适用于单机多线程,也适用于分布式集群,相比更新锁机制要更加简单一些。
后台更新机制还适用于刚上线的业务去进行缓存预热。缓存预热指的是系统上线之后,将相关的缓存数据直接加在到缓存系统,而不是等待用户访问才来触发缓存加载。
缓存热点
虽然缓存系统本身的性能比较高,但对于一些特别热点的数据,如果大部分甚至所有的业务请求都命中同一份缓存数据,则这份数据所在的缓存服务器的压力也很大。例如,某明星说他xx了,就会有很多人来围观。
缓存热点的解决方案就是复制多份缓存副本,将请求分散到多个缓存服务器上,减轻缓存热点导致的单台缓存服务器的压力。以微博为例,对于粉丝数超过100w的明星,每条微博都会生成100份缓存,缓存的数据是一样的,通过在缓存的key里面加上编号进行区分,每次读缓存时都随机读取其中某份缓存。
缓存副本有一个细节需要注意,就是不同的缓存副本不要设置统一的过期时间,否则就会出现所有缓存副本同时生成同时失效的情况,从而引发雪崩。