缓存和避免缓存风暴 - 多个同时计算

7

我们有一个非常昂贵的计算需要缓存。因此,我们做了类似于以下操作:

my $result = $cache->get( $key );

unless ($result) {
    $result = calculate( $key );
    $cache->set( $key, $result, '10 minutes' );
}

return $result;

现在,在执行calculate($key)期间,在我们将结果存储在缓存中之前,会有其他几个请求同时开始运行calculate($key),因为许多进程都在计算相同的内容,所以系统性能受到影响。
想法:让我们在缓存中放置一个标志,表示正在计算一个值,因此其他请求只需等待该计算完成,然后它们都可以使用它。类似于以下内容:
my $result = $cache->get( $key );

if ($result) {
    while ($result =~ /Wait, \d+ is running calculate../) {
        sleep 0.5;
        $result = $cache->get( $key );
    }
} else {
    $cache->set( $key, "Wait, $$ is running calculate()", '10 minutes' );
    $result = calculate( $key );
    $cache->set( $key, $result, '10 minutes' );
}


return $result;

现在这开启了一个全新的问题。如果 $$ 在设置缓存之前就死了呢?如果,如果...所有这些问题都是可以解决的,但由于 CPAN 中没有做到这一点(CPAN 中有关于 任何 事情的东西),我开始想:是否有更好的方法?例如,为什么 Perl 的 CacheCache::Cache 类不提供类似这样的机制?是否有一种可靠的模式可以使用?理想情况下,应该有一个已经在 squeeze 中打包的 CPAN 模块或一个突破口,让我看到我的错误... :-) 编辑:我后来了解到这被称为 缓存失效 并更新了问题的标题。

IPC::ShareLite 提供了 SysV 共享内存的面向对象接口。它有点类似于 Cache,提供了独占锁定。 - tuxuday
有一篇关于“缓存惊群”的维基百科文章和一个关于避免此问题的Perl-Cache讨论主题。 - Peter V. Mørch
还有一种Django片段:MintCache策略。 - Peter V. Mørch
4个回答

2

flock()函数。

由于您的工作进程都在同一系统上,您可以使用传统的文件锁定来串行化昂贵的calculate()操作。作为奖励,此技术出现在几个核心文档中。

use Fcntl qw(:DEFAULT :flock);    # warning:  this code not tested

use constant LOCKFILE => 'you/customize/this/please';

my $result = $cache->get( $key );

unless ($result) {
    # Get an exclusive lock
    my $lock;
    sysopen($lock, LOCKFILE, O_WRONLY|O_CREAT) or die;
    flock($lock, LOCK_EX) or die;

    # Did someone update the cache while we were waiting?
    $result = $cache->get( $key );

    unless ($result) {
        $result = calculate( $key );
        $cache->set( $key, $result, '10 minutes' );
    }

    # Exclusive lock released here as $lock goes out of scope
}

return $result;

好处:工人死亡将立即释放锁$lock

风险:LOCK_EX可能会永久阻塞,这需要很长时间。避免SIGSTOP,可以尝试使用alarm()

扩展:如果您不想序列化所有calculate()调用,而只是为了相同的$key或某些键集的所有调用,您的工作人员可以flock()/some/lockfile.$key_or_a_hash_of_the_key


我曾担心过例如工人死亡时会发生什么。# Exclusive lock released here as $lock goes out of scope 真是聪明!谢谢! - Peter V. Mørch
同意,在适当的情况下使用flock()非常方便。所有的魔法都在底层文件描述符中,无论是进程死亡还是Perl在作用域末尾引用文件句柄的销毁,都能完全满足需求。 - pilcrow

1

使用锁定?或者可能会过度杀伤力?或者如果可能的话,离线预计算结果,然后在线使用?


1

虽然对于您的使用情况可能有些过度,但您是否考虑使用消息队列进行处理?RabbitMQ 看起来是 Perl 社区目前比较流行的选择,并且它通过 AnyEvent::RabbitMQ 模块得到支持。

在这种情况下,基本策略是每当您需要 计算 新密钥时,向消息队列提交请求。如果您只能可靠地处理一个密钥,那么队列可以被设置为仅同时 计算 一个密钥(按请求顺序)。或者,如果您可以安全地并发计算多个密钥,则队列也可以用于合并同一密钥的多个请求,计算一次并将结果返回给所有请求该密钥的客户端。

当然,这会增加一些复杂性,并且 AnyEvent 调用需要与您可能习惯的编程风格有所不同(我可以提供一个示例,但我自己从未真正掌握它),但它可能提供足够的效率和可靠性收益,使这些成本值得您的努力。


肯定也是一个有价值的途径。但我认为它需要融入更大的画面,而我们现在还没有准备好。感谢提醒。 - Peter V. Mørch

-1

我基本上同意pilcrow上面的方法。 我想再加一件事情:调查使用memoize()函数来潜在地加速代码中的calculate()操作。

有关详细信息,请参见http://perldoc.perl.org/Memoize.html


Memoize在一个进程中运行良好。正如OP中提到的,这个问题涉及到多个进程都想要计算相同的东西。除非我误解了什么,Memoize不会将缓存值提供给不同的进程,因此在这种情况下没有用处。是的,它可以使用绑定哈希,但我猜它会遇到来自OP的确切问题。 - Peter V. Mørch

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接