【发布时间】:2009-05-06 11:20:18
【问题描述】:
我已经阅读了几本关于实现 php 缓存系统的指南(我的网站是自定义编码的,查询量很大并且不断增长),其中包括:http://www.snipe.net/2009/03/quick-and-dirty-php-caching/
我完全理解它们,但我无法缓存页面的某些部分,最好的方法是什么?
【问题讨论】:
-
您正在寻找的是部分缓存,请查看下面我的答案,了解如何做到这一点。
我已经阅读了几本关于实现 php 缓存系统的指南(我的网站是自定义编码的,查询量很大并且不断增长),其中包括:http://www.snipe.net/2009/03/quick-and-dirty-php-caching/
我完全理解它们,但我无法缓存页面的某些部分,最好的方法是什么?
【问题讨论】:
这是我的提示:创建一个对象或函数,让您可以根据其名称缓存“部分”。这样,您可以缓存部分页面,并将呈现部分包含在 if 块中。我所做的是对传入的文本进行哈希处理并将其用作'./cache' 中的文件名,并返回是否需要重新生成的布尔值;您显然需要为此进行输出缓冲。
这会给你一个缓存框架,
if(Cache::cached('index-recent-articles', 5 /* minutes */)) {
Cache::start();
echo 'stuff here';
Cache::stop('index-recent-articles');
} // And if Cache::cached could echo the cached HTML when the result is false...
// then this is a tidy else-less bit of code.
我不知道这是否是最优的,像 memcache 这样的基于服务器的解决方案会更好,但这个概念应该对您有所帮助。显然,您如何管理缓存,无论是按文件还是按数据库或扩展名,都取决于您。
编辑:
如果您希望使用基于文件的缓存,那么一个如此简单的系统可能就是您所需要的。显然我没有在你的鞋子上测试过,这主要是从我的脑后拉出来的,但就目前而言,这对于你可能想要的东西来说是一个不错的开始。
abstract class Cache {
const path = 'cache/';
static function start() { ob_start(); }
static function end($id, $text = null) {
$filename = sprintf('%s%u', Cache::path, crc32($id));
file_put_contents($filename, $text === null ? ob_get_clean() : $text);
}
static function cached($id, $minutes = 5) {
$filename = sprintf('%s%u', Cache::path, crc32($id));
$time = $minutes * 60;
if(time() - filemtime($filename) > $time) {
return true;
} else {
echo file_get_contents($filename);
return false;
}
}
}
【讨论】:
如果您进行大量读取但很少更新,缓存会很有用。数据库中的数据更改得越频繁,缓存系统的问题就越多。缓存确实为您的代码库增加了一定的复杂性,这可能会让人很难处理。在最坏的情况下,它甚至会降低您的网站速度。
最重要的问题是:
什么时候必须使缓存无效?它什么时候变得陈旧?在大多数情况下,如果数据库查询返回的行与您缓存该页面时不同。但是你怎么知道的?你没有(也许有办法,但我想不出任何 atm),因为要检查它,你可能必须查询结果进行比较。
你可以做什么:
每次更新数据库的相关部分时清除所有缓存
如果您的数据库很少更新——每小时、每天、每周,这确实是可能的。但如果变化不断出现,那就没用了。大多数网络项目都是如此。
发生某事后清除缓存的项目
这仅在不必立即反映更改的情况下才有效(例如,如果有一段时间不正确数据并不重要)。在这种情况下,如果某个项目超过 X 分钟或超过 Y 次浏览量,您只需清除该项目的缓存即可。
只清除相关部分
在这里,您必须确定更新数据库时缓存的哪些部分受到影响。如果操作正确,更改会在性能提高的同时立即得到反映。
最可能的是选项 3:你必须找出答案。因此,作为示例,让我们以博客的经典案例为例,它由首页、存档页面和每个条目的详细信息页面组成。
变化由管理面板(条目的crud)和cmets引入
如果条目被编辑或删除,您必须清除缓存:
如果有人发表评论,您只需清除详细信息页面,但前提是索引或存档中未显示 cmets 的数量。否则,与 entry-crud 相同。
如果站点范围内的某些内容发生更改,则必须清除整个缓存(糟糕!)
现在,让我们考虑一下 entry-crud 和存档。如果档案的类型是“每月一页”,则清除条目所属的月份。但如果存档是条目 1-10、11-20、21-30... 最有可能必须重建整个存档缓存。
等等……
一些问题:
如果您没有正确识别所有受影响的部分,可能会导致数据过时和/或(未)死链接。
如果更新过于频繁,构建缓存是额外的工作,因为当下一次浏览量发生时,缓存很可能再次过时并且无论如何都必须重建。
页面的某些部分不适合缓存,例如(自定义)搜索功能。如果缓存在其他地方也能正常工作,一切都会很快很好,但搜索仍然非常慢。
如果您在发生大量请求时必须清除整个缓存,则可能会出现问题。然后它可能会阻塞您的服务器,因为缓存未命中通常比页面一开始没有缓存的情况更昂贵。更糟糕的是,如果有 3 个请求进来,而第一个请求在处理其他两个请求之前无法缓存页面,那么缓存会被请求 3 次而不是一次。
我的建议:
优化您的数据库。键和配置好吗?也许它可以在没有缓存的情况下工作。
优化您的查询。 “解释选择”!
只缓存页面的一部分——昂贵的部分。使用 str_replace 和占位符填充小而便宜的更改
如果一切正常,请使用 apc 或 memcached 代替文件(文件通常工作得很好,但 apc/memc 更快)。您还可以使用您的数据库来缓存您的数据库,通常效果很好!
您是在构建惰性缓存系统还是急切缓存系统? lazy 表示:页面第一次请求时建立缓存, eager 表示:更新后。
嗯,我对你没有任何真正的建议。太依赖问题了:)
有一个使用键 256 的博客条目请求。它显示了博客条目、cmets 和当前登录的用户。查询条目和 cmets 以及格式化所有文本和所有内容的成本很高。当前登录的用户驻留在会话中。
首先,为要缓存的部分创建一个唯一键。在这种情况下,缓存键可能是条目的数据库 ID(带有一些前缀和后缀)。
因此,缓存文件的名称应为cache/blogentry_256.tmp。检查该文件是否存在。
如果它不存在,请执行所有昂贵的查询和格式化,在当前用户的名称应该是的位置留下一个占位符(例如 {username})并将结果保存到cache/blogentry_256.tmp。注意不要将任何不应该向所有人显示或每次请求都更改的数据写入此文件。
现在,读取文件(或重复使用 1 中的数据)并将用户名 str_replace 放入占位符。 echo 结果。
如果某个条目被更改或有人发生变化,您必须删除带有条目 id 的缓存文件。
这是惰性缓存 - 仅当用户请求页面时才会构建缓存。小心 - 如果用户在评论中输入 {username},它也会被插入其中!这意味着,您必须在 str_replacing 之后转义缓存数据并取消转义。这种技术也适用于 memcached 或 apc。
问题:您必须围绕该缓存设计来构建您的设计。例如如果您想显示“5 分钟前发布的评论”而不是“5 月 6 日下午 3:42 添加评论”,那么您就有麻烦了。
【讨论】:
为什么不能缓存它们?如果是因为它们变化非常快,那么您最好尝试减少检索和渲染开销。
如果内容因用户而异,那么您可以为每个用户提供不同的缓存版本。例如,如果您使用对象缓存,则在缓存键中包含用户标识符或其他一些唯一值。如果您使用更通用的 HTTP 级缓存,例如 Squid,则可以将 Vary 标头设置为,例如Vary: Cookie,这将导致代理仔细检查它向谁提供内容。
您仍应指示缓存和代理不要在公共缓存中存储任何敏感信息。
【讨论】:
请记住,缓存不是单一的解决方案 - 您的应用程序可以在不同级别上进行缓存,具体取决于变量的范围。在单个请求中,您可能有一个身份映射来防止对相同数据的额外查询。您可以使用 memcached 跨请求缓存数据。数据库本身有一个查询缓存,以及其他类型的低级缓存。您的渲染引擎还可以在不同级别缓存 - 整个页面、页面的各个部分等。您需要弄清楚可以缓存的内容并为此制定策略。
此外,与所有优化一样,请确保在调整任何内容之前进行测量。这将确保 1) 你实际上改进了事情,而不是让事情变得更糟,以及 2) 你专注于重要的事情,而不是其他所有事情。
【讨论】: