
最近踩了几天缓存的坑,发现这玩意儿看起来简单,真要用好,里面的门道比我想的深多了。这篇把第3天的学习整理一下,重点说说缓存失效为什么真的那么难。
第1天聊了大图(客户端-服务端、扩展性、延迟vs吞吐量),第2天进了数据库(SQL vs NoSQL、复制、分片)。今天的课题本来以为早就懂了:缓存。结果发现我只懂了最容易的那20%。
核心思想很简单:取数据是有代价的(数据库查询、网络调用、计算),所以把结果复制一份存在访问更快、更便宜的地方,每次直接读这份副本,不用重复干活。
但"把副本存在某个更快的地方"背后藏着一堆决策:存哪儿、存多久、怎么知道这个副本已经不准了?
我梳理了一下典型系统中缓存出现的不同层级,比想象中多:
我注意到一个规律:缓存离用户越近,速度越快,但越容易过期,而且失效越难控制。
几个反复出现的模式:
没有哪个策略是放之四海而皆准的"正确答案"。Cache-aside是我默认会选的,但当数据陈旧不可接受时write-through更合适,当写入吞吐量比持久性保证更重要时write-behind更合适。
这里那句著名的话才真正让我理解了:计算机科学里只有两件困难的事——缓存失效、命名、 Off-by-one 错误(这个笑话本身就是对命名和计数有多难的吐槽)。失效之所以难,是因为你要回答"真实数据源变了吗",又不能一直去检查真实数据源,否则缓存就失去意义了。
我研究的几种策略:
一个之前没太注意的问题:当一个热门缓存条目过期瞬间,大量请求同时穿透到数据库——因为它们同时没命中缓存。这叫缓存雪崩(也叫惊群效应),是个很好的例子:缓存本来是保护数据库的工具,但如果不注意,反而会在最糟糕的时刻给数据库最大压力。常见的缓解方案包括过期时间随机化,或者"锁"机制——只有一个请求去重建缓存,其他请求等着。
缓存表面看是性能优化,但实际上是个穿着性能优化外衣的一致性问题。每个缓存决策本质上都是在问:你愿意让数据有多陈旧、陈旧多久,来换取速度?
这和第1天(CAP定理)和第2天(复制)遇到的取舍问题是一模一样的形状。开始感觉系统设计的大部分内容就是少数几种取舍反复穿着不同的马甲出现。
下一个课题:负载均衡 —— 流量是怎么真正分配到多台服务器的、不同的负载均衡算法、以及如果不小心负载均衡器本身会成为单点故障。
如果你在生产环境被缓存失效bug坑过,欢迎分享——这类教训往往是最难忘的。