什么是管理密钥(在memcache中)以防止陈旧缓存值的最佳方法?

时间:2009-12-10 09:01:08

标签: php caching memcached

我最近在我的网站上实现了memcache,它一直在重载mysql(mysql已尽可能优化)。它解决了我所有的负载问题,并且网站运行良好。

我现在面临的问题是过时的缓存值。我在大多数页面上都有1小时的自动过期时间,而且当数据库中的值为时,我也会删除密钥,但是我很难跟踪并有效地清除所有密钥。

在某些页面上,它是微不足道的。我可以将密钥设为item_id(例如item_4653),当更新数据或删除项目时,密钥将被清除。

但是在大​​多数页面上,我使用脚本filename + querystring,md5,并将其用作memcache中的键。这对于复杂的URL(非常常见)特别有用。

例如,我已加载以下页面。

的index.php SEARCH_KEYWORDS = &安培; search_section = 1&安培;排序=释放&安培;页= 2

它将包含一个项目列表,这些项目将从memcache中获取。然后另一个用户提交一个项目,其标题中包含“ good ”,它恰好位于值的范围内,它将显示在第2页上,除非它不会出现在那里,直到缓存已刷新。更复杂的是,新添加的项目也会出现在index.php?sort = newest,以及index.php?category = some_category?page = 1等等。每个项目都有一个独特的key(脚本名称的md5 +查询字符串)。

因此,新添加的项目可能会显示在几十个页面上,如果它们是从实时数据库中提取的,但在更新陈旧缓存之前,它们不会在任何页面上显示。唯一的选择是等待项目自动过期。

这个问题在我的论坛(自定义编码)上变得更加明显,其中值必须根据需要更新,以用于所有可能的缓存页面组合。假设我有4页线程,我注意到第2页上有3个垃圾邮件帖子。删除它们后,第2页重建,但是它还必须重建第3页和第4页,否则新重建页面上会有重复的帖子2和旧页面3.这只是一个例子.....有几十个这样的场景。

有什么想法吗?

6 个答案:

答案 0 :(得分:14)

由于您在memcached中缓存整个页面,因此您的页面无法相互共享数据库中的缓存数据。假设我有page1.php和page2.php,其中 page1 page2 作为memcached中的键。两个页面都显示。我添加了一个新项目。现在我必须到期第1页第2页

相反,我可以在memcached中有一个键,page1.php和page2.php都用于显示项目。当我添加新项目时,我会使项目键失效(或更好,更新它的值),并且page1.php和page2.php都是最新的。

如果您仍想缓存整个页面,可以向密钥添加信息,这些信息会在缓存数据发生变化时发生变化(如果数据变化太频繁,则无效)。例如:

"page1:[timestamp of newest item]"

通过这种方式,您可以查找最新项目的时间戳,便宜的查询,并使用它构建缓存密钥。添加新项目后,缓存键将更改,自动过期。此方法意味着您仍然必须每次都按下数据库以查看最新项目的时间戳。

答案 1 :(得分:2)

您可以从memcached密钥的更简单的命名方案中受益 - 因此它们更容易删除。似乎与MD5解决方案一样,您可能会为通常显示相同数据的内容创建太多密钥。

你也可以考虑更短的缓存时间,比如20分钟?

此外 - 您为每个搜索结果页面检索每页有多少项?如果你有一个分页搜索 - 从服务器获取50个项目不应该太强烈。

您可能已经调整了mysql服务器,但是您是否调整了查询​​(通过检查EXPLAIN输出来改进它们)或表结构(通过添加有用的索引)?

我也想知道这些页面上的查询有多强烈。你加入几张桌子吗?您可以从更简单的查询 - 或一些查询(如下所述)中受益。

或者 - 对于结果中的每一行,您是否运行另一个查询 - 或几个?您可以从稍微复杂的搜索查询中受益,避免您必须执行嵌套查询。或者,您是否被ORM库咬了,它执行相同的操作,运行搜索,然后在每次迭代时查询子项?

'一些简单的查询'解决方案 - 例如 - 如果你有一个项目,并想知道它在结果集中的类别......

取而代之的是:

SELECT i.id, i.name,
c.category FROM items AS i
INNER JOIN categories AS c
ON i.category_id = c.id;

这是一个简单的例子 - 但是说有类别,还有其他几个JOIN。

你可能会走这条路:

// run this query
SELECT id, category FROM categories - and put that into a keyed array.

// then in PHP create an array keyed by the id
$categories = array();

while ( false !== ( $row = mysql_fetch_assoc ( $result ) ) ) 
{
  $categories[ $row['id'] ] = $row['category'];
}

// and so on
$types = array(); // ...
// etc.

然后进行搜索,但没有所有的JOINS,只需从带有where子句的items表中,并在输出中说...

<?php foreach($items as $item): ?>
  <h4><?php echo $item['name']; ?></h4>
  <p>Category:  <?php echo $categories[ $item['category_id'] ]; ?></p>
  <p>Type:  <?php echo $types[ $item['type_id'] ]; ?></p>
  <!-- and so on -->
<?php endforeach; ?>

这是一个小贫民区,但也许这个 - 以及其他建议 - 将有所帮助。

答案 2 :(得分:2)

Memcached :: set有一个expire参数。也许您可以将此默认值设置为一小时,但对于返回搜索结果的页面 - 或者在论坛中,您可以将其设置为更短的时间段。

答案 3 :(得分:2)

你可以做一些简单的事情:

首先,如果您确实希望将查询字符串用作缓存键,请使其更具确定性和可预测性。我这样做是通过对查询字符串进行排序,例如:?zed=7&alpha=1转换为?alpha=1&zed=7。同时删除与缓存密钥无关的变量。

要处理?page参数的问题,以及由于缓存未刷新而未显示的项目,我有几个想法:

Folke将缓存键添加“版本”的想法很有效。同样的技巧用于轻松制作像未访问的链接。

另一种方法是将页面数存储在缓存值中,然后在更新数据库时迭代缓存键。

cache.put("keyword,page=3", array(num_pages=7, value=...))

...later...
update_entry()
num_pages, value = cache.get("keyword,page=3")
for i in num_pages:
  cache.flush("keyword,page="+i)

这是否是一个好主意取决于有多少页面,以及循环运行时更新的可能性。

第三个想法是缓存整个结果集而不仅仅是结果页面。根据结果​​集的大小,这可能是也可能不是一个选项。更新结果集后,只需刷新该关键字的缓存即可。

cache.put("keyword", array(0="bla", 1=foo", ...)
...later...
cache.get("keyword")[page_num]

第四个想法是更改缓存后端并使用构建的东西来处理这种情况。我不知道其他缓存服务器是什么,所以你必须环顾四周。

最后,为了补充所有这些,您可以尝试更聪明地了解缓存条目的到期时间。例如,使用更新之间的平均时间,或关键字的每秒查询次数等。

答案 4 :(得分:1)

您可以采取哪些措施来确保缓存始终是最新的,而无需对代码进行大量更改,这可以使用“版本缓存”。这确实会增加你要做的memcache请求的数量,但这可能是你的解决方案。

此解决方案的另一个好处是您可以将到期时间设置为永不过期。

这个想法是基本上有一个版本号存储在memcache中,在你的情况下是一个特定的关键字(每个关键字,而不是组合)。怎么用呢?

当有人提交新项目时:

  • 对于标题中的每个字词,请if(!Memcache:increment("version_" + keyword)) {Memcache:set("version_" + keyword);}

当有人执行查询时:

  • 你正在做的md5事情已经确定了。此外,您需要将搜索字符串中每个关键字的版本添加到memcache密钥。

这可确保只要关键字有新结果(或删除时更少),版本就会受到影响,因此所有相关的内存缓存都会被查询。

缓存始终是最新的,查询可能会在缓存中停留超过1小时。

答案 5 :(得分:0)

缓存失效是一个大问题

  

&#34;计算机科学中只有两个难题:缓存   失效和命名事物。&#34;

我会给你一些想法,引导你完全解决,因为没有针对所有用例的一般解决方案..

  • 了解varnish esi和X-Article-id https://www.varnish-software.com/blog/advanced-cache-invalidation-strategies
  • 使用nginx ssi
  • 跟踪缓存中的所有项目,因此如果您缓存100个论坛消息存储db中的每个消息id,那么您有类似lastMessages的内容包含消息:1,2,550,123等..现在当任何项目更新时搜索其商店并逐个清除它们(或重建它们并再次存储)
  • 它就像是同一个解决方案,但不是每个缓存项都知道它的商店,每个模型应该知道它的商店在哪里