避免在使用Cache时发生缓存挥发刚结束的突增的方法
关于Cache利用政策的一般论点
恰当地利用缓存对于提高整个系统的性能非常有效。同时,通过巧妙地利用缓存,还可以减轻难以扩展的部分的负载压力。
积极地利用缓存可以包括使用CDN等情况,以下是一些例子:
[前提条件]Cacheのヒット率が高いケース
マスタデータの参照など、実行時にほとんど結果が変わらないケース
どのユーザーに対してもほぼ同じページを返答して良いケース
[前提条件]Cacheが古くても大きな問題とならないケースまたは古い状態のCacheの更新が出来るケース
Cache保持期間を短くすることである程度解消可能
DBを更新した時にCacheの内容も書き換える事ができるケース(Read Through Cacheとして利用している場合は、Cacheの削除だけで良い。)
リソース負荷が大きい箇所に適用するケース
レイテンシの高いシステムとの結合が発生するケース
スループットの低いシステムとの結合が発生するケース
CPUリソースを大きく消費する処理が発生するケース
请注意,虽然我们将缓存的命中率设置得很高,但是通过延长缓存的保存时间来提高缓存的命中率的方法往往是一个反例,请谨慎考虑。
-
- 保持期間を長くしても、線形にしかヒット率は上昇しない
-
- ヒット率の低い無駄なデータでシステムリソース(Memory/Storage)が占有されてしまう
-
- Cacheのミスヒット時にもCacheの参照コスト、更新コストは発生してしまう
- 保持期間を長くすることで古いデータにアクセスする不整合状態が発生しやすくなる
如果相反地,针对高点击率的数据进行缓存时,即使只保持非常短的时间(如1到10秒),也能产生足够的效果,因此在应用程序设计时提高缓存的点击率变得非常重要。
例如,如果将一项每秒1,000次访问的处理更改为使用缓存保持1秒钟,(如果使用正确)该处理的执行次数将从1,000次/秒减少到1次/秒。
阅读关于Cache中缓存未命中的峰值解决方案。
通过使用缓存,可以将系统的特定部分负载分散,并进行配置。然而,在超时设置等情况下,缓存一旦挥发,可能会导致大量访问流入先前被绕过的繁重处理部分。
例:如果一个处理每秒有1,000个访问量的操作的延迟为1秒,如果缓存失效无法使用,则该操作的执行次数将有1,000个rps的请求流入。
如果上述操作需要10秒钟,则同时流入10,000个请求。
如果缓存被正确使用,当只有一个请求同时发生时,会发生系统挂起的情况,因为有10,000个请求涌入其中。
如果出现这样的症状,请考虑在应用程序中添加一个机制,在进行Cache更新处理之前锁定相应的键,以确保Cache更新操作同时只有一个请求发生。
另外,通过设置缓存的保持期限与默认设置不同,可以尝试更新仅限于已经过了一定时间的第一个请求,并使其他请求使用现有的缓存数据。

以下是使用PHP进行Memcache示例的代码:
这是一个使用PHP调用Memcache的示例代码。
MemcacheWithLockSample.php 可以通过加锁功能示例来进行简化。
<?php
/**
* レイテンシの大きな処理をCache利用することで迂回するサンプル
*/
// Cacheの生存期間は20秒であるが、10秒経過した時点でCacheの更新を行うことができるようになる
$mc = new MemcacheWithLock("localhost:11211", 20, 10);
// まずCacheから取得
if(!$value = $mc->get("random_value")){
// Cacheに無かった場合は関数実行
$value = someHighLatencyFunc();
// Cacheに格納
$mc->set("random_value", $value);
}
echo $value."\n";
/**
* レイテンシの大きな処理
*/
function someHighLatencyFunc(){
sleep(5);
return rand(0,10000);
}
/**
* Memcacheラッパークラス
* 仮想的なロック機構対応版、ロックを取得できなかったプロセスはキャッシュ上のデータを参照する
* ロック時間を設けることで、ロックし続ける事を防止する
* このサンプルでは、falseの値をCacheするとキャッシュにヒットしなかったことになることに注意
*/
class MemcacheWithLock{
private static $mc = null;
private $expire_sec = 0;
private $lock_sec = 0;
function __construct($memcache_hosts, $expire_sec, $lock_sec){
$this->expire_sec = $expire_sec;
$this->lock_sec = $lock_sec;
if(!self::$mc){
$mc = new Memcache();
$memcache_hosts = explode(",", $memcache_hosts);
foreach($memcache_hosts as $memcache_host){
list($host,$port) = explode(":", $memcache_host);
$mc->addServer(trim($host), trim($port));
}
self::$mc = $mc;
}
}
public function set($key, $value){
// memcacheに格納するデータに、ロック時間を付加する
$_with_lock_value = array(
'unlock_time' => time() + $this->lock_sec,
'value' => $value,
);
return self::$mc->set(
$key,
$_with_lock_value,
0,
time() + $this->expire_sec
);
}
public function get($key){
if($_with_lock_value = self::$mc->get($key)){
if(
isset($_with_lock_value['value']) &&
isset($_with_lock_value['unlock_time'])
){
$unlock_time = $_with_lock_value['unlock_time'];
$value = $_with_lock_value['value'];
if ($unlock_time > time()) {
// ロック中はそのまま値を返答する
return $value;
} else {
// ロックが終了していたら、再ロックをかけるが、そのプロセスにおいては取得失敗とし、再度データを取得する
// 他のプロセスは残りのlock期間は継続してデータ取得が可能
$this->set($key, $value);
return false;
}
}
}
// 最初は空のロックだけを作成する
$this->set($key, false);
return false;
}
public function del($key){
return self::$mc->delete($key);
}
}
在这个样本程序中,如果缓存本身不可用,并且在该情况下同时有多个请求流向缓存数据的重新获取处理,那么这仅在该缓存在一定时间内没有更新的情况下才会发生,因此它允许突然发生大量访问的情况作为罕见情况承受。
如果这种情况成为问题,请实施自旋锁机制,在无法获取数据的情况下,将其设定为一定时间进行休眠,然后再次尝试从缓存中获取。