一般来说,网站随着访问量以及数据库的增大,访问速度将会越来越慢,如何优化这个响应速度,增大用户支持容量是网站从小到中,到大的必经之路。
你也可能听说过对于大型web站点一般严重依赖于cache来弹性放大其基础设施的能力。虽然已知大型网站都使用了cache,但是很少有比较严谨的探讨性文章来说说他们是如何来做这个cache的。其中的原因可能有几个,一方面很多cache的策略都是特定于特定网站的,很难泛化成理论。另一方面可能过于复杂,并无放之四海而皆准的策略存在,特别是cache的过时更新问题到现在为止应该还没有一个非常好的解决方案。
本文介绍几种应用广泛的缓存策略,如果使用得当,应该可以保证永远不会有过期数据访问的问题存在。其中第一个策略叫做:write-through策略,第二种叫做:generational caching.
writethrough是最简单有效的策略。当你向db写的时候,你就顺便向cache更新相应key的数据。这样的好处是,任何随后的请求总是会击中cache,而不用再打到db上返回数据。只有一种情况会打到db: cache内存已经占满,而你访问的key的value已经被清场。该策略也有一些需要注意的地方:
一般人们都会使用object id作为cache的key,但是不同的表,其id可能相同,因此在构造cache key时需要注意这一点,严格定义并且遵循一个统一的key定义规范就很重要了,比如User/17
另外,任何put/delete操作时,首先得确认该操作是否成功执行了,只有确认成功执行了,你才能去更新cache,
否额,可能就会出现db并未更新,但是cache却被更改的情况,从而db和cache就不再一致!
虽然这个策略对单个的object做cache非常有效,但是大部分应用会从db中拉取多个对象,比如(返回所有属于张三的书籍).对于操作包含多个对象的数据,我们来谈谈另一种策略"generational caching"
设想你在设计一个blog app,可能需要一个简单的posts表格包含每一个Post的信息,比如title, createdat等。一般博客类型网站需要有两种页面,一个是单博客页面,一个是博客聚类页面。比如posts/3和posts/两个页面。问题就来了,我们如何有效地对posts/这个页面的数据做好cache呢?
对单博客页面的caching前面介绍的策略非常适合:write through即可,每一个post都将映射为一个cache key(一般都是基于object type和object id,比如Post/3),而每次对一个post的更新都将write-through回cache中,保持数据的一致性。
更困难的问题是:我们如何处理博客聚类列表页面,比如home page或者category page的数据缓存?为了保证db和cache的一致性,非常重要的一点是:任何时候一个Post被update,那么所有包含该Post引用的keys都必须expired掉。而这是并不容易的事情,因为一个post可能在多个cache key中被引用,比如: latest ten posts, posts in php category, posts favorited by user 15等。虽然理论上你可以通过写代码来找到包含已更新post的所有cache key,并且手工去expire掉他,但是必须指出这个过程是非常费事费力也非常容易错误的!我们推荐另外一种思路,这个思路就是genrational caching.
这种generational caching对每种类型的object去维护一个"generation" value。每次一个object被更新,则该generation value就会被增加。依然使用post作为例子,任何时候有人更新了一个post object,我们就增加the post generation object as well. 然后,任何时候我们从cache中读或者写一组boject时,我们就包含这个generation key.
通过包含这个generation value在cache key中,任何时候一个Post被更新,那么接下来的访问就不会击中cache,因此强制回源db获取数据。这种策略的一个结果是:任何时候一个post object被更新或者删除,所有包含多个posts的keys都会隐含expired掉。之所以说是隐含,因为我们永远不会主动去删除这些objects,仅仅通过增加这个generation value,我们就能保证所有old keys永远不会被访问,下面的事情就留给cache系统本身的垃圾回收机制去处理。
在多个实际application中应用该策略后,对于cache的性能提高有以下几点认识:总的来说该策略可以大大提高应用的性能减少数据库的负载。可以节约大量的表格扫描等高密度计算能力要求。同时由于减少了对数据库的请求,其他必须访问数据库的请求也能大大加快性能。
为了保持cache的一致,这个策略相对有些保守,也就是说有可能部分不该过期的cache entry也被强制过期了。比如,如果你更新了一个特定类目下的一个Post,本策略会将所有类目的key都过期。而这看起来会有些低效率和过优化嫌疑。而我也发现大多数应用是read-heavy(重读请写)的因此这种过优化的缓存策略本身不会带来大的问题。相反,如果不适用这种略显“过优化”的简易策略,那么我们的代码实现完全是和应用,model绑定在一起的,是非常难以维护的。
我前面提过在这个策略中任何model都不会被显式地从cache中删除。这也隐含着和caching tool以及库满驱赶策略有关。一般这个cache策略会使用LRU(least recent used) eviction policy来配合使用。一个LRU策略会对那些老的keys优先从内存中移除。