今日内容
从今天开始,我们开始讲另一个重要的话题:性能。【性能】是指“如何让系统变得更快”。
从【读性能】和【写性能】两块来做展开,帮助大家识别并提升自己的系统能力。我们先从【读性能】开始。由于读性能的内容比较多,我们会分为两期来讲。
所谓【读性能】,也就是“查询数据的性能”。我们会从【缓存神器】、【读写分离】、【并发思维】、【异步设计】、【产品设计】这五个方面来给大家展开。
今天我们来讲其中的第一个,也是最重要的一个,那就是【缓存神器】。
01缓存用法多
缓存是大家最容易想到的提升读性能的办法。而且,使用缓存往往立竿见影,对读性能的提升产生奇效,所以我把他称之为“神器”。
缓存在具体的使用时,有很多种方式:
从上图中我们可以看到,缓存既可以用于对DB数据的缓存、也可以用于对业务计算结果的缓存、还可以用于对返回结果的缓存。
缓存的本质是:用空间替换时间。这和很多算法有着相似的思路。
02【本地缓存】和【中心缓存】
缓存又可以分为【本地缓存】和【中心化缓存】。
如图所示,【本地缓存】和【中心化缓存】各有利弊。在真实的生产环境中,这两者往往一起使用。
【本地缓存】常用于对元数据和配置的加载(这些数据相对稳定,没有数据频繁变更下的一致性问题,同时缓存的数据量也不大,内存hold得住),避免系统读取这些数据需要反复和DB打交道。
【中心化缓存】用于对用户请求数据的缓存。可以缓存较大的数据量,同时,可以避免一致性问题。
02缓存更新的难题
缓存利器虽然强大,但是使用起来却要非常小心,不然就会有意想不到的效果。
在你的印象中,缓存的典型用法应该是这样的:
我们先读取缓存,如果读不到数据,我们则会读DB,然后再把读出来的数据更新到缓存中。
这是典型的缓存使用方式,但这里有个大漏洞,那就是如果DB中的数据更新了,缓存里还是旧数据,要怎么办呢?
上图是我们常用的一种方式。在写入缓存时,我们设置一个过期时间,时间一到,缓存会自己删除数据,继而通过读请求加载新的数据到缓存中。
这种方式实现简单,不容易有坑。但他的问题在于,新数据写入DB后,需要等待旧数据到达过期时间后才能被访问到。这个就比较“看脸”了,如果过期时间设置的长了些,新数据可能会滞后很久。
一种直观的思路就是:是否能够在数据写入DB时,同时更新cache中的数据呢?
当然可以,如下图:
这样是不是就完事大吉了呢?一个坑正向你走来 。
如果出现下图这样的情况,缓存里的值依然是有问题的。请跟着下图的序号来看看“两个更新数据的请求是如何挖坑的”。
图中蓝色的线携带的值是一个更加“新鲜”的值。但是由于蓝色线执行得比较快,优先写入了缓存,然后被“动作较慢”的黄色线最后覆盖成了旧值。
惊不惊喜?意不意外?
那我们要怎么解决这个问题呢?
很简单,我们只要把【更新缓存】的动作换成【删除缓存】就可以了。像下图这样:
这是经典的【cache aside pattern】缓存更新策略。在写入DB的时候淘汰缓存,在读取DB的时候写入缓存。简单地说就是:写淘汰 + 读更新。
我们终于可以安稳睡觉了!不!还是有问题!
请大家仔细看下面这张图,按照步骤来看:
根据上图的顺序可以看到,最终读请求写入cache的缓存还是旧的。
想睡个好觉怎么就那么难呢?
所以最终,你还是不得不通过一些手段,保证在各种情况下,缓存里的值还是可以被正确更新。怎么弄呢?写入缓存时还是加个过期时间呗!
一路看下来,你是不是觉得缓存的问题特别的费劲。没错,各种读写并发,缓存里的值就像迷一样难以捉摸。
但是,我们还是有一个绝招的。那就是记住一个宗旨:缓存里的任何数据都不能够永久地待在里面。
你可以通过过期时间删除、可以通过定时淘汰缓存、也可以通过定时刷新缓存这些方式。总之,任何一个被你扔到缓存里的东西,都要保证有被拿出来或者被覆盖的一天。
03缓存穿透
在使用缓存的时候,我们期望缓存可以挡住绝大部分流量,以分担DB的压力。一个你期望的理想情况应该如下
这样:
但是,人生吧,意外总是不期而遇,比如下图这样:
从图中可以看到,如果有大量不存在的key的请求,那么缓存完全拦截不住,所有流量就都打到了DB,DB就挂了。
这就是【缓存穿透】。也就是针对这样的请求,缓存完全没有抗流的能力。
那要怎么应对这样的问题呢?要知道,这样的问题既可能是上游bug,也可能是恶意攻击。
你有两种办法可以应对,我们来看一下第一种办法是:缓存空值
之所以之前的方案会把流量打到DB,是因为缓存里拿不到后,DB里也拿不到。DB里拿不到就不会往缓存里写。
缓存空值就是在DB读不到数据的时候,写一个特殊的标记到缓存中,这样缓存就知道DB里也没值,直接返回请求。
第二种方法是:布隆过滤器
布隆过滤器是专门用来判断一个元素是不是在集合中的。如上图所示,通过将DB数据加载到布隆过滤器中(只加载查询的key),布隆过滤器就具备了识别请求key是否有必要继续访问的能力。
你也许会说,“用布隆过滤器我还不如用一个set呢,存下所有的key,然后访问时候判断在不在set里就可以了”。
没错,你这样做事可以的,但布隆过滤器需要的内存更少,判断也更快。在大请求量的场景下,有非常好的性能。
关于布隆过滤器的细节我不在这里做展开,有兴趣的同学可以自己学习一下,它的设计思路非常巧妙,很有意思。
04缓存雪崩
在应对完【缓存穿透】问题后,你敢拍着胸脯说没问题了不?你肯定不敢了吧?你的判断没错,你起码还要解决另一个问题,那就是【缓存雪崩】。
所谓的【缓存雪崩】,就是指缓存突然之间起不到任何抗流作用了。原因在于服务端而非请求方。比如下面两种情况:
【情况一:缓存挂了】
这种情况很容易理解,缓存突然直接崩了,所以流量打到DB。
【情况二:缓存集中过期】
缓存里的数据设置了相同的过期时间,一到时间,突然“集体失踪”。这种情况较多见于“通过定时批量刷新缓存”或者“缓存预热”的情况。
应对这两种情况,我们一般有如下这样的方案。
针对【缓存挂了】的情况我们可以使用如下两种:
上图左侧是指,我们可以对缓存进行分片。这样的话,如果某个分片挂掉了,也只会影响一部分的缓存数据,不会整体雪崩。
上图右侧是指,你可以使用成熟的缓存中间件。像redis、memcached都有自己的高可用方案,值得信赖。
针对【缓存集中过期】的情况,我们也有两种方案。
上图左侧是指,我们在写缓存的时候,可以设置一个随机的过期时间。一次来避免缓存同时失效。
上图右侧是指,我们可以通过主动刷新的方式,在缓存过期前重置他们的过期时间。事实上,如果你采用这样的方式,你甚至可以不用设置缓存过期时间(因为定时刷新的动作就可以保证原来的缓存数据不会永久生效)。
05缓存当DB用?
一般来说,在我们的印象里,缓存是不可靠的。因为缓存之所以快,是基于内存,而内存断了电就忘记了所有。但是,现在流行的缓存中间件都有不错的高可用方案(多节点冗余+持久化),那缓存可以当做DB来用吗?在一些场景下是可以的,比如:海量商品的查询。
例如很多电商网站,流量大+产品多。当然你可以使用NoSQL数据库来支撑,但是如果流量实在太大,并且有一定的关联查询和筛选逻辑,要怎么做呢?还是得靠缓存。可以使用如下这样的方案:
图中我标明了,场景需要满足:只读 + 高可用。
上图中,因为服务器所有的数据都来自于缓存,缓存的高可用要求毋庸置疑。而只读的话,可以避免写数据的丢失。就算是使用redis以及其配套的持久化方案,在极端情况下依然会有数据的丢失。
但是,真的必须是“只读”才能用缓存替代DB吗?
我们到写性能的篇章中会给大家再来说说。
今日小结
今天,我们开始讲系统性能方面的内容。我们把性能分为【读性能】和【写性能】。
【读性能】中又有很多的话题,我们今天把所有的篇幅都放在了【缓存】上。缓存可以说是提升系统【读性能】的关键手段。
我们讲了使用缓存时你会遇到的一些坑。包括:更新缓存的各种意外、缓存的穿透、缓存的雪崩。我们带着大家一起看了他们发生的原因以及应对的方式。
此外,我们还提到,缓存在一些场景下可以当做DB来使用,只要你把重点关注到就可以了。