
1 缓存的保质期
昨夜腹中饥饿,本想打开冰箱找点儿蛋黄派充饥,好不容易在角落里寻到,喜不自胜想果腹一顿,无意中瞥了一眼包装:擦,缓存过期了,不能吃了。
急冲冲跑到楼下小卖部,想着应个急,没想到老板娘说:售罄,想要只能明天了。哎?我就想吃个蛋黄派,咋就这么难?
回到家,我微微颤抖着双手伸向发霉的蛋黄派,临到嘴边,最终含泪扭过头去。鲁迅说过:过期的蛋黄派不能吃。虽然也可能吃了没问题,但万一拉肚子了呢,对吧?特别是对我这种有前科的......肚子。缓存也一样,过期了不建议食用。
你们就说我坚定不坚定吧?
严格说来,缓存需要的是淘汰策略,可以基于时间(保质期),也可以基于空间。
2 读五级缓存 写缓冲限流
前一些日子梳理秒杀要点,发现缓存这一块似乎从未正经总结过。于是趁着今天太阳金灿灿像个蛋黄派,整理一下。我把缓存从前端到后端分为五级,史称五级缓存论:
1 Chrome
当你肚子饿了,首先想到的肯定是先翻翻冰箱,冰箱没有才是喊妈去楼下小卖部,可见离得近是非常大的优势。缓存同理,浏览器可是离用户最近的地方,而且也是唯一的离线缓存的机会。
至于我为什么以chrome为题,是因为这玩意的市场占有率已达6~7成,到了挟天子以令诸侯的地步。chrome从54版本开始,用户刷新页面的时候,只要缓存不过期,都是从memory cache或disk cache中直接加载。只有过期的资源,才需携带ETag或Last-Modified送审web server,以获取更新的资源。
我们可以做一个实验:打开chrome的Developer Tools,切到Network面板。随便打开一个网址(比如:https://github.com/lixianmin),然后刷新一下页面。观察Status这一列,你会发现大部分都是返回200,而不是304。观察Size这一列,你会发现大部分都是从memory cache或disk cache中加载。
如果你在调试web app,或者发现web资源一直刷新失败,可以考虑Ctrl+F5强制刷新或Disable Cache
跟资源(jpg, js, css等)相关的缓存主要分为两种:
强制缓存:针对静态资源,通过
Cache-Control: max-age=3600
设置相对过期时间。类似的还有一个Expires指令,用于设置绝对过期时间,但因为时钟漂移问题,如今已弃用。另外,在服务器使用Set-Cookie指令设置cookie时,可以使用Max-Age与Expires指令设置cookie的超时时间,与强制缓存的指令简直一模一样。协商缓存:针对动态资源,通过ETag(计算资源hash值)和Last-Modified(秒级时间戳)向server查询资源有没有变化,如果没有变化就返回304。
除了页面资源外,js代码也经常需要缓存一些字段,比如JWT令牌用于实现免登录。这通常可以基于sessionStorage或localStorage实现,需要注意的是sessionStorage是可以抗Page Refresh的。另外,因为缓存可能需要ttl或加密,可以考虑使用localstorage-slim等三方代码库。
2 CDN
CDN是整个五级缓存架构中唯一的真分布式缓存,跨小网络运营商甚至全球部署,实现就近访问,抗压能力值得信赖。在秒杀场景下,即使大家为了抢商品强刷chrome(浏览器缓存失效),如果能把流量引导到CDN,也无需担心后端服务被打垮。
CDN适用于缓存静态资源,如图片、视频等,并且各大云服务场景还提供了一些有用的增值服务,比如图片裁切、格式转换、视频流媒体加速、DDos防御、WAF防火墙等,而且价格平民,谁用谁受益,早用早超生。
由于存储成本很低,目前CDN上的静态资源存储模式已逐渐演变为:只增加,不修改,永不过期。通过在资源名称中嵌入digest值,可以做到每一份新的资源都拥有不同的文件名。而且天然防止预测文件名称,在一定程度上避免了CDN上的资源被扫描的可能性。
3 Nginx
反向代理、七层负载是Nginx的常规用法,但我们也可以把它当作缓存使用。用得好可以化腐朽为神奇,毕竟在抗压这一块Nginx可是从来没服过谁,经过007弹性工作制的加持,号称全年无休小能手,任劳任怨少白头。
使用proxy_cache_key,Nginx可以配置请求粒度的缓存,支持秒级过期时间,可以把大量动态请求拦截在Nginx层,从而有效缓解应用服务器的请求压力。
而且,不用写代码,配置一下就行了,你说给力不给力?
4 Local Cache
Local Cache即内存缓存,在java中也有人称为jvm缓存。针对规模小且变化频繁的数据,Local Cache可以在第一时间侦测到变化,并实时调整缓存内容。
在极限优化的场景下,Local Cache可以直接缓存编码之后的字节流。这样,当有前端需要获取数据时,可以直接通过TCP链接返回,而不需要做任何额外的数据转换和编解码工作,可以有效缓解CPU的工作负载。
Local Cache的缺点是,它会破坏微服务的无状态化设计。在典型的微服务架构设计中,为了避免单点问题,我们通常会把同一微服务部署到多个节点。使用Local Cache就意味着,同一缓存在多个对等微服务节点同时存在,会增加一定的内存成本。更重要的是,多个对等微服务节点以不同的节凑更新各自的Local Cache,它们之间的数据一致性是比较差的。
5 Redis
对比local cache,Redis的容量大、一致性好,但是只支持string等比较原始的数据类型,通常需要后端服务在内存中重新组装后才能返回给前端使用。
Redis是一个很神奇的东西,几乎是后端的必备利器,但又被误会颇深。经常有人把Redis摆出奇奇怪怪的姿势,用在一些匪夷所思的地方。比如有人敢为天下先,用Redis代替MySQL,就有点儿藐视CAP定理了。
到目前为止,我自己观察到的适合Redis的用途只有两类:一是缓存,二是统计。
缓存是Redis的日常用法,不做展开,如果所述,只要记得加过期时间就好。
统计是指点赞数、PV、UV、BloomFilter之类的用法。其特点有二:一是占用空间不大,因此可即使长期存储也不会产生大key问题;二是允许偶然误差,即使因为NPC等问题导致些许误差,也不影响PM或用户对业务的判断。
3 缓存就像Show Girl
总是有同学问起:如果从缓存中的数据与DB里的不一样怎么办,会不会影响正常业务?比如我按照某东页面上看到的价格下单,如果后台价格变了不就买不到了嘛?
在这里重申一下:缓存的数据与DB不一致是常态,除了用户id、订单id之外,参与计算的数据从来都是直接来自于DB,而不会使用从前端传过来的那一份。缓存就像Show Girl,看看就行了,真下单的时候您买的也不是Show Girl不是?
有钱的老板也可以私聊哈
我这里虽然没有Show Girl,但我写过不少Show Girl的文章,而且都是免费的,都放在这里了,老板您要不要关注一下?

9 References
关于缓存和 Chrome 的“新版刷新”




