如果这是第二次看到我的文章, 欢迎右侧扫码订阅我哟~
本文长度为 3578 字, 建议阅读 10 分钟.
坚持原创, 每一篇都是用心之作~
此前的「伸缩性」章节结束了, 此文是「高性能」章节的第一篇.
只要是位正儿八经的程序员自然知道「缓存」是什么, 甚至我司的很多做运营的小姐姐现在和程序员小哥哥的交流中都时不时冒出「缓存」字眼, 让人压力山大.(本文讨论的「缓存」皆指的是软件层面运用的缓存)
大家都知道的一点是, 缓存可以让原本打开很慢的页面, 变得能 "秒开". 你平时访问的 App, 网站几乎都有涉及到缓存的运用.
那么, 缓存除了能加速数据的访问之外, 还有什么作用呢?
另外, 任何事物都有两面性, 我们如何才能将缓存的优点发挥得淋淋尽致, 同时避免掉到它的弊端中呢?
Z 哥今天想分享给你的就是我对缓存的理解和运用的思路, 希望对你有所启发.
「缓存」能做什么?
正如前面所说, 大家最普遍的理解就是当我们遇到某个页面打开很慢的时候, 会想到引入缓存, 这样页面打开就快了.
其实快和慢都是相对的, 从技术角度来说, 缓存之所以快是因为缓存是基于内存去建立的, 而内存的读写速度比硬盘快 X 倍, 所以用内存来代替硬盘作为读写的介质自然能大大提高访问数据的速度.
这个过程大致是这样的, 通过在内存中存储访被问过的数据供后续访问时使用, 以此来达到提速的效果.
其实除此之外, 缓存还有另外 2 个重要的运用方式,「预读取」和「延迟写」.
预读取
预读取就是预先读取将要载入的数据, 也可以称作「缓存预热」. 就是在系统对外提供服务之前, 先将硬盘中的一部分数据加载到内存中, 然后再对外提供服务.
为什么要这样做呢? 因为有些系统一旦启动就要面临上千上万的请求进来(在一些 toC 的项目尤其如此), 如果直接让这些请求打到数据库上, 非常大的可能是数据库压力暴增, 直接被干趴, 无法正常响应.
为了缓解这个问题, 需要通过「预读取」来解决.
可能你会问, 哪怕用了缓存还是扛不住呢? 那就是做横向扩展 + 负载均衡的时候到了.(可以点击文末链接阅读之前的《弹性架构》系列)
如果说「预读取」是在「数据出口」加了一道前置的缓冲区的话, 那么顾名思义, 下面要说的「延迟写」就是在「数据入口」后面加了一道后置的缓冲区.
延迟写
你可能知道, 数据库的写入速度是慢于读取速度的, 因为写入的时候有一系列的保证数据准确性的机制.
所以, 如果想提升写入速度的话, 要么做分库分表, 要么就是通过缓存来进行一道缓冲, 再一次性批量写到磁盘, 以此来提速.
题外话: 由于分库分表对跨表操作以及多条件组合查询的副作用巨大, 所以引入它的复杂度远大于引入缓存, 我们应当优先考虑引入缓存的方案.
那么, 通过缓存机制来加速 "写" 的过程就可以称作「延迟写」. 就是预先将需要写入到磁盘或者数据库的数据, 先暂时写入到内存, 然后就返回成功. 再定时将内存中的数据批量写入到磁盘.
可能你会想, 写到内存就认为成功, 万一中途出现意外, 断电, 停机等导致程序异常终止的情况, 数据不就丢了吗?
是的. 所以,「延迟写」一般仅用于对数据完整性要求不是那么苛刻的场景. 比如点赞数啊, 参与用户数啊等等, 可以大大缓解对数据库频繁修改所带来的压力.
其实在我们熟知的分布式缓存 Redis 中, 其默认运用的持久化机制 --RDB, 也是这样的思路.
在一个成熟的系统中, 能够运用到缓存的地方其实并不是一处. 下面 Z 哥就来帮你梳理一下我们在哪些地方可以 "加缓存".
哪里可以加「缓存」?
在说哪里可以加缓存之前我们先搞清楚一个事情, 我们要缓存什么? 也就是符合什么特点的数据才需要加缓存? 毕竟加缓存是一个额外的成本投入, 得物有所值.
一般来说你可以用这两个标准来判断: 热点 (被高频访问, 如几十次 / 秒以上) 数据, 静态 (很少变化, 读远大于写, 如几天变更一次) 数据.
接下去就可以替它们找到合适的地方加缓存了.
缓存的本质是一个 "防御性" 的机制, 而系统之间的数据流转是一个有序的过程. 所以, 选择在哪里加缓存就相当于选择在一条马路的哪个位置设路障. 在这个路障之后的道路都能受到保护, 不被车流碾压.
那么在以终端用户为起点, 系统所用的数据库为终点的这条道路上可以作为缓存设立点的位置大致有以下这些.
每个设立点可以挡掉一些流量, 最终形成一个漏斗状的拦截效果, 以此保护最后面的系统以及最终的数据库.
下面 Z 哥来简要描述下每一个的运用场景以及需要注意的点.
浏览器缓存
这是离用户最近的可以作为缓存的地方, 而且借助的是用户的 "资源"(缓存的数据在用户的终端设备上), 性价比可谓最好, 让用户帮你分担压力.
当你打开浏览器的开发者工具, 看到 from cache 或者 from memory cache,from disk cache 的时候, 就意味着这些数据已经被缓存在了用户的终端设备上了(没网的时候也能访问到一部分内容就是这个原因).
这个过程是浏览器替我们完成的, 一般用于缓存图片, JS,CSS 这些. 我们可以通过 Http 消息头中的 Cache-Control 来控制它, 具体细节这里就不展开了.
JS 里的全局变量, 以及 cookie 等运用也属于该范畴.
浏览器缓存是在于用户侧的缓存点, 所以我们对其的掌控力就差很多, 在没有发起新请求的情况下, 你无法主动去更新数据.
CDN 缓存
提供 CDN 服务的服务商, 在全国甚至是全球部署着大量的服务器节点(可以叫做「边缘服务器」).
那么将数据分发到这些遍布各地服务器上作为缓存, 让用户访问就近的服务器上的缓存数据, 就可以起到压力分摊和加速效果. 这在 ToC 类型的系统上运用, 效果格外显著.
但是需要注意的是, 由于节点众多, 更新缓存数据比较缓慢, 一般至少是分钟级别. 所以一般仅适用于不经常变动的静态数据.
题外话: 解决方式也是有的, 就是在 url 后面带个自增数或者唯一标示, 如? v=1001. 因为不同的 url 会被视作 "新" 的数据和文件, 被重新 create 出来.
网关 (代理) 缓存
到这里做缓存就是在你自己的地盘了. 很多时候我们会在源站前面架一层网关(或者说反向代理, 正向代理), 为的是做一些安全机制或者统一分流策略的入口.
同时这里也是做缓存的一个好场所. 毕竟网关是 "业务无关性" 的, 它能够拦下来的请求, 对背后的源站也是很大的受益, 减少了大量的 CPU 运算.
常用的网关 (代理) 缓存有 Varnish,Squid,Ngnix. 一般情况下, 简单的缓存运用场景, 用 nginx 即可, 因为大部分时候我们会用它来做负载均衡, 能少引入一个技术就少一份复杂度嘛. 如果是大量的小文件可以使用 Varnish, 而 Squid 则相对大而全, 运用成本也更高一些.
进程内缓存
一个请求能走到这里说明他是 "业务相关" 的, 需要经过业务逻辑的运算.
也正因为如此, 从这里开始对缓存的引入成本比前面 3 种大大增加, 因为对缓存与数据库之间的「数据一致性」要求更高了.
可能我们大多数程序员第一次刻意使用缓存的场景就是这个时候. 进程内和进程外的缓存运用中有很多的细节需要注意, 这些也是我们后续文章的内容, 所以我们后续再详聊.
进程外缓存
这个大家也熟悉, 就是 Redis,Memcached 之类, 甚至也可以自己单独写一个程序来专门存放缓存数据, 供其他程序远程调用.
同样, 这里的细节我们后续再聊, 这里先多说几句关于 Redis 和 Memcached 该怎么选择的建议.
对资源 (CPU, 内存等) 利用率格外重视的话可以使用 Memcached, 但程序在使用的时候需要容忍可能发生的数据丢失, 因为是纯内存的机制. 如果无法容忍这点, 并且对资源利用率也比较豪放的话可以使用 Redis. 而且 Redis 的数据库结构更多, Memcached 只有 key value, 更像是一个 nosql 存储.
数据库缓存
数据库本身自带缓存模块的, 否则也不会叫它内存杀手, 基本上你给多少内存就能吃多少.
数据库缓存是数据库的内部机制, 我们这里就不深入下去了. 一般都会给出设置缓存空间大小的配置来让你进行干预.
最后, 其实磁盘本身也有缓存. 所以你会发现, 为了让数据能够平稳的写到物理磁盘中真的是一波三折, 不知道什么时候可以有 "快" 到不需要程序来考虑缓存的磁盘出现来拯救我们程序员呢.
「缓存」是 Silver bullet 吗?
可能你会想缓存那么好, 那么应该多多益善, 只要慢就上缓存来解决?
一个事物看上去再好, 也有它负面的一面. 缓存也有一系列的副作用需要考虑. 除了上面提到的「缓存更新」和「缓存与数据的一致性」问题, 还有诸如:
缓存雪崩
缓存穿透
缓存并发
缓存无底洞
缓存淘汰
...
等等问题, 这些 Z 哥会在接下去的文章中和你一起深入剖析.
想第一时间了解这些的可以「关注」Z 哥一波~
总结
好了, 我们总结一下.
这次呢, Z 哥先向你介绍了运用缓存的三种思路.
然后梳理了在一个完整的系统中可以设立缓存的几个位置, 并且分享了关于浏览器缓存, CDN 缓存, 网关 (代理) 缓存的一些使用经验.
希望对你有所启发.
后续的文章我将着重深入「进程内缓存」和「进程外缓存」的最佳实践, 等我再次出现.
来源: https://www.cnblogs.com/Zachary-Fan/p/cache.html