缓存 怎么工作
假设一本常看的书,每次要看都得跑到远处的仓库去取。每来回一趟,都要花上好一阵。可要是在书桌上放一本副本,以后伸手就能拿到。把常用的数据复制一份放在近处、需要时很快取出,这个办法就是缓存。
远处的仓库来回慢
上一课,我们看了怎样安全地修改数据。这一课讲读取。数据库通常把数据存在磁盘上,或者在另一台计算机上运行,所以要取数据,就得发出请求,再等回答。
这一来一回,比想象的要久。就算再看同样的数据,也得像第一次那样走一趟远路。下面去远处的仓库跑几趟看看。
点数据,从远处的仓库取出来。路很长,来回一趟要花好一阵,再点一次也一样慢。
一两次还能忍。可要是同样的数据,比如热门商品的信息,一天要查几十万次,每查一次都得走一趟远路。等待的时间越积越多,仓库那边也得没完没了地回答同一个问题,忙个不停。
再说,大部分数据很少用到,只有一小部分用得特别频繁;刚用过的东西马上又用到,也很常见。既然这样,只把常用的那一小部分放在近处,不就行了?
在近处放一份副本
办法很简单:把常用的数据从远处的仓库取来一次,然后在近处放一份副本。这个近处的存放处,就叫缓存。
缓存通常比仓库快得多,可也相应地小得多。内存比磁盘快,同一台计算机里比另一台计算机近。所以,只挑常用的东西,放进又快又小的地方。
点一个项目,在近处的缓存里放一份副本。原本空着的缓存格子会一个个填上。
近处的缓存里有了副本。以后再找同样的数据,就不必跑到远处的仓库,从旁边的缓存里取出来就行。
不过,要找的东西不一定总在缓存里。在缓存里和不在缓存里,要做的事不一样,下面分开来看这两种情况。
命中与未命中
要找的东西已经在缓存里,就走短路直接取出,这叫命中。不在缓存里,就只能去远处的仓库跑一趟,这叫未命中。
未命中的时候,顺便把取来的副本留在缓存里,下次再找同样的东西就会命中。所有请求中命中的比例叫命中率,要看缓存发挥了多大作用,就看这个数。
点一个请求。在缓存里就走短路命中;不在,就走长路未命中,然后在缓存里留一份副本。把同一个请求点两次,就能看出区别。
一开始未命中,很慢;留下副本之后,同样的请求就命中了,变快了。越是常找的东西,命中越多,平均时间也就越短。
缓存满了,为了给新副本腾地方,就得把某样东西挪出去。常见的做法是挪走最久没用过的那一个,这条规则叫 LRU,意思是最近最少使用。它来自这样一个想法:刚用过的东西,很可能马上又会用到。不过,还剩下一个麻烦问题:远处仓库里的原件变了,缓存里的副本会怎样?
缓存过时的时候
缓存只是副本。远处仓库里的原件变了,缓存并不知道,还拿着以前得到的那份副本。
这样一来,命中虽然快,给出的值却可能是旧的。这种状态叫缓存过时。快是快了,却有给出错误值的风险,这就是缓存的短处。
点原件,把值改一改。缓存还拿着旧值;重新填缓存或者把它扔掉,就又和原件一致了。
原件一改,缓存就对不上了。防止的办法大致有两种:一种是原件一变,就把缓存里的副本改成新值或者删掉;另一种是给副本定一个有效期,时间一过就扔掉。
扔掉后空出来的位置,会在下一次未命中时填上新副本。有效期定得短,旧值少了,未命中却会变多;定得长,情况正好相反。不管哪种,都是要找到一个平衡,既减少旧值,又保住快速的命中。
小结
归纳起来是这样:远处的存储来回路长,所以慢,常用的数据就在近处放一份副本,这就是缓存。要找的东西在缓存里,就是命中,很快;不在,就是未命中,跑一趟远处,再把副本留下。
缓存满了,就从最久没用的开始挪走;原件变了,就改掉或扔掉副本,挡住旧值。放在近处、用得快,又不让它和原件对不上,这就是缓存。
依次点要点回顾一下。(远处慢 → 在近处放副本 → 命中与未命中 → 原件一变就过时)
缓存不只放在数据库前面。CPU 里、网页浏览器里,还有把远处服务器的内容复制到用户附近的分发网络里,都用着同样的想法。
要快,把数据压小和放在近处同样重要。怎样用更少的字节存放、传送同样的内容,下一课讲压缩时会说明。