-->

左耳听风_059_58_性能设计篇之缓存

你好,我是陈浩网名左耳朵house.我在前面呢分享了分布式系统设计模式系列文章的前两个部分弹力设计篇和管理设计篇。

那今天呢我会开始这一系列的最后一部分内容啊,也就是性能设计篇,主题叫做性能设计篇是缓存。

那基本上来说呢,在分布式系统中最耗性能的地方就是最后端的数据库了。

一般来说啊,只要小心维护好数据库四种操作中的三个写操作,insert update和delete不太会出现性能问题,insert呢一般不会有性能问题。

而update和delete呢一般会有主键,所以呢也不会太慢,除非索引进的太多。

而数据库里的数据啊又太多,这三个操作才会变慢。

在绝大多数情况下,select是出现性能问题最大的地方。

一方面呢select会有很多像jorain啊、group order,还有like等这样丰富的语义。

而这些语义呢是非常耗性能的。

另一方面呢,大多数应用啊都是读多写少,所以呢就加剧了慢查询的问题。

分布式系统中的远程调用呢也会消耗很多的资源,因为网络开销会导致整体的响应时间下降。

那为了挽救这样的性能开销呢,在业务允许的情况下,使用缓存呢是非常必要的事情。

从另一个方面说呢,缓存在今天的移动互联网中啊是必不可少的一部分。

因为网络质量呢不一定永远是最好的,所以前端呢也会为所有的API加上缓存。

不然呢在网络不通畅的时候,没有数据,前端呢都不知道该怎么展示UI了。

既然移动互联网会有网络质量的问题,就导致我们呢必须容忍数据的不实时性。

那么从业务上来说啊,在大多数情况下都是可以使用缓存的那缓存呢是提高性能最好的方式。

一般来说啊缓存会有catch a site read right through,还有right behind cash三种更新模式。

首先呢我们来聊一聊catch le side的更新模式,那这个呢是最常用的设计模式了。

它的具体逻辑呢有三个。

那第一个呢是失效,就是应用程序先从catch取数据呃,如果没有取到呢,则从数据库中取数据成功之后呢,放到缓存中。

那第二个呢是命中,也就是应用程序从始中取数据取到之后呢返回。

那第三个呢是更新,就是先把数据存到数据库中成功之后呢,再让缓存失效。

这里呢我在文中放了两幅图来让你参考。

那这个呢是标准的设计模式,包括facebook的论文schedule, man、 cash and facebook啊也使用了这个策略。

那为什么不是写完数据库之后再更新缓存呢?你可以点开文章这个链接,看一下correa上这个回答,那主要呢是怕两个并发的血操作导致脏数据,那么是不是这个cache set就不会有并发问题了呢?啊,不是的,比如说一个呢是读操作,但是没有命中缓存,这样呢就会到数据库中去取数据。

而此时呢来了一个写操作,写完数据库之后呢,让缓存失效。

然后之前的那个读操作,再把老的数据给放进去。

那这样呢就会造成脏数据这个案例啊在理论上会出现,但实际上出现的概率啊可能非常低。

因为这个条件啊需要发生在读缓存时缓存失效,而且呢有一个并发的写操作,实际上呢数据库的写操作会比读操作啊慢得多。

而且呢还有锁表读操作,必须在写操作之前进入数据库操作,又要晚于写操作更新缓存。

那所有这些条件都具备的概率啊并不大。

所以呢这也就是二上那个答案里说的,要么呢通过cortwo PC或者pixel协议保证一致性,要么呢就是拼命的降低并发时脏数据的概率。

而facebook呢使用了这个降低概率的玩法,因为two PCE太慢,而pixels呢太复杂。

那当然呢最好啊还是为缓存设置好过期的时间。

那接着呢我们来说一说reay through和restthrough更新模式。

我们可以看到在前面的开车side的套路中呢,应用代码需要维护两个数据存储,一个是缓存,一个是数据库。

所以呢应用程序啊就比较啰嗦。

而race through和rrestthrough的套路呢是把更新数据库的操作由缓存自己代理了。

所以呢对于应用层来说啊,就简单很多了。

我们可以理解为啊应用认为后端就是一个单一的存储。

而存储呢自己维护自己的cathe. Ring through的套路呢就是在查询操作中更新缓存。

也就是说呢,当缓存失效的时候,casacside是由调用方负责,把数据加载进缓存。

而read through呢则用缓存服务自己来加载,从而对应用方是透明的。

而rstthrough的套路呢和rinthrough le相仿。

不过啊是在更新数据的时候发生。

当有数据更新的时候啊,如果没有命中缓存呢,直接更新数据库,然后再返回。

如果命中了缓存呢,则更新缓存,然后由始自己来更新数据库。

我们来看文中这张来自维基百科的pg词条的图片。

那其中的memory呢,你可以理解为就是我们例子里的数据库讲完了前面两种模式啊,那接着呢我们来说第三种模式叫做right behind开始,right behind又叫right back.那一些了解linux操作系统内核的同学呢,对write back应应该是非常熟悉。

这这不就是linux文件系统的page cash算法吗?是的,你看啊基础知识全都是相通的,所以呢基础很重要。

我已经说过不止一次了。

而read back的套路呢就是在更新数据的时候,只更新缓存,不更新数据库,而我们的缓存呢会异步的批量更新数据库。

那这个设计的好处呢,就是让数据的IO操作飞快无比。

因为是直接操作内存嘛,因为是异步啊,所以rack back还可以合并对同一个数据的多次操作,所以性能的提高呢是相当可观的。

但是它带来的问题呢,就是数据不是强一致性的,而且呢可能会丢失。

在软件设计上呢,我们基本上不可能做出一个没有缺陷的设计,就像算法设计中的时间换空间,空间换时间一个道理。

有时候呢强意志性和高性能、高可用和高性能是有冲突的,软件设计呢从来都是trade off.另外呢read back back的实现逻辑比较复杂,因为它需要track,有哪些数据是被更新过的,需要刷到持久层上。

那操作系统的write back呢,会在紧张这个case需要失效的时候,才会把它真正的持久起来。

比如内存不够了,或者呢进程退出了等情况。

那这个呢又叫lazy, right.从文中这个read back的流程图中呢,我们可以看到它的基本逻辑。

最后呢我们来说一说缓存设计的重点,缓存更新的模式呢,基本上就像前面所说的那样。

不过呢这还没完,缓存啊已经成为高并发高性能架构的一个关键组件了。

那现在呢很多公司都在用REDIS来搭建他们的缓存系统。

那一方面呢是因为REDIS的数据结构比较丰富。

那另一方面呢,我们不能在service里面放local cash,呃,一呢是每台机器的内存啊不够。

大二呢是我们的service,有多个实例,负载均衡器会把请求啊随机分布到不同的实力里,而缓存需要在所有的service实例上都建好。

这就让我们的service啊有了状态,更难以管理了。

所以在分布式架构之下呢,一般啊都需要有一个外部的缓存集群。

那关于这个缓存集群呢,你需要保证的是内存要足够大,网络带宽也要好。

因为缓存本质上来说啊,是一个内存和IO密集型的应用。

另外呢如果需要内存很大,那么你还要动用数据分片技术,来把不同的缓存啊分布到不同的机器上。

那这样呢可以保证我们的缓存集群可以不断的scale下去。

那关于数据分片的事呢,我会在后面讲述缓存的好坏呢,要看命中率,缓存的命中率高呢就说明缓存有效。

那一般来说呢命中率到百分之八十以上就算很高了。

那当然呢有的网络为了追求更高的性能,要做到百分之九十五以上,甚至呢可能会把数据库里面的数据啊几乎全部装进缓存中,这当然啊是不必要的啊,也是没有效率了。

因为通常来说呢热点数据只会是少数。

另外呢缓存是通过牺牲强一志性来提高性能的那这个世界上任何事情啊都不是免费的,所以并不是所有的业务都适合用缓存。

那这个呢就需要在设计的时候仔细调研好需求。

使用缓存提高性能,就是会有数据更新的延迟,缓存数据的时间周期呢也需要好好的设行,太长太短都不好,过期期限啊不宜太短。

因为可能会导致应用程序不断的从数据存储检索数据,并且将它添加到缓存。

呃,同样呢过期期限不宜太长。

因为这样呢会导致一些没人访问的数据还在内存中不过期,从而浪费内存。

使用缓存的时候呢啊一般都会使用LRU策略。

也就是说,当内存不够,需要有数据被清除,内存的时候,会找最不活跃的数据来清除。

那所谓最不活跃呢,意思就是最长时间没有被访问过了。

所以呢开启LIU策略会让缓存在每个数据访问的时候呢,把它调到前面。

而要淘汰数据的时候呢,就从最后面开始淘汰。

于是呢对于LIU的缓存系统来说啊,它需要在key value这样的非顺序型的数据结构中维护一个顺序的数据结构。

并在读缓存的时候呢,需要改变被访问的数据在顺序结构中的排位。

于是呢我们的LIU在读写的时候呢都需要加锁。

所以呢LIU可能会导致更慢的缓存存取的时间,那这一点呢一定要小心。

最后呢我们的世界呢是比较复杂的,很多网站呢都会被爬虫光顾,我们要小心这些爬虫。

因为这些爬虫呢可能会爬到一些很古老的数据,而程序呢会把这些数据加到缓存中去,而导致缓存中那些真实的热点数据被挤出去。

那对于这一点呢,一般来说啊,我们需要有一个爬虫保护机制,或者我们引导这些人去使用我们提供的外部API在那边呢,我们可以有针对性的做多租户的缓存系统。

也就是说呢把用户和第三方开发者的缓存系统分离开来。

好了,我们来总结一下今天分享的主要内容。

首先呢缓存是为了加速数据访问在数据库之上添加的一层机制。

然后呢,我讲了几种典型的缓存模式,包括cache or side read right through,还有right behind cash,还有它们各自的优缺点。

最后呢我介绍了缓存设计的重点。

除了性能之外呢,在分布式架构下和沟网环境下,对缓存集群一致性,LU的锁竞争、爬虫等多方面都需要考虑。

在下一节课呢,我们会讲述异步处理,希望能对你有帮助。

也欢迎你分享一下你接触到的缓存方式有哪些呢?怎样权衡一致性和缓存的效率呢?文末呢我给出了分布式系统设计模式系列文章的目录,希望能在这个列表里啊找到自己感兴趣的内容。