Redis/Memcached高并发访问下的缓存失效时可能产生Dogpile效应(Cache Stampede效应).
推荐阅读:高并发下的 Nginx 优化方案 http://www.linuxidc.com/Linux/2013-01/78791.htm
Memcached的read-through cache流程:客户端读取缓存,没有的话就由客户端生成缓存.
Memcached缓存示例:
$mc = new Memcached();
$mc->addServers(array(
array(‘127.0.0.1‘, 11211, 40),
array(‘127.0.0.1‘, 11212, 30),
array(‘127.0.0.1‘, 11213, 30)
));
$data = $mc->get(‘cached_key‘);
if ($mc->getResultCode() === Memcached::RES_NOTFOUND) {
$data = generateData(); // long-running process
$mc->set(‘cached_key‘, $data, time() + 30);
}
var_dump($data);
假如上面的generateData()是耗时3秒(或更长时间)的运算或数据库操作.当缓存服务器不可用(比如:缓存实例宕机,或网络原因)或是缓存失效瞬间,如果恰好有大量的访问请求,那就会出现机器CPU消耗或数据库操作次数短时间内急剧攀升,可能会引发数据库/Web服务器故障.
避免这样的Dogpile效应,通常有两种方法:
Memcached使用”锁”的示例:
function get($key) {
global $mc;
$data = $mc->get($key);
// check if cache exists
if ($mc->getResultCode() === Memcached::RES_SUCCESS) {
return $data;
}
// add locking
$mc->add(‘lock:‘ . $key, ‘locked‘, 20);
if ($mc->getResultCode() === Memcached::RES_SUCCESS) {
$data = generateData();
$mc->set($key, $data, 30);
} else {
while(1) {
usleep(500000);
$data = $mc->get($key);
if ($data !== false){
break;
}
}
}
return $data;
}
$data = get(‘cached_key‘);
var_dump($data);
上面的处理方法有个缺陷,就是缓存失效时,所有请求都需要等待某个请求完成缓存更新,那样无疑会增加服务器的压力.
如果能在数据失效之前的一段时间触发缓存更新,或者缓存失效时只返回相应状态让客户端根据返回状态自行处理,那样会相对比较好.
下面的get方法就是返回相应状态由客户端处理:
class Cache {
const RES_SUCCESS = 0;
const GenerateData = 1;
const NotFound = 2;
public function __construct($memcached) {
$this->mc = $memcached;
}
public function get($key) {
$data = $this->mc->get($key);
// check if cache exists
if ($this->mc->getResultCode() === Memcached::RES_SUCCESS) {
$this->_setResultCode(Cache::RES_SUCCESS);
return $data;
}
// add locking
$this->mc->add(‘lock:‘ . $key, ‘locked‘, 20);
if ($this->mc->getResultCode() === Memcached::RES_SUCCESS) {
$this->_setResultCode(Cache::GenerateData);
return false;
}
$this->_setResultCode(Cache::NotFound);
return false;
}
private function _setResultCode($code){
$this->code = $code;
}
public function getResultCode(){
return $this->code;
}
public function set($key, $data, $expiry){
$this->mc->set($key, $data, $expiry);
}
}
$cache = new Cache($mc);
$data = $cache->get(‘cached_key‘);
switch($cache->getResultCode()){
case Cache::RES_SUCCESS:
// ...
break;
case Cache::GenerateData:
// generate data ...
$cache->set(‘cached_key‘, generateData(), 30);
break;
case Cache::NotFound:
// not found ...
break;
}
上面的memcached缓存失效时,只有一个客户端请求会返回Cache::GenerateData状态,其它的都会返回Cache::NotFound.客户端可通过检测这些状态做相应的处理.
需要注意的是:”锁”的TTL值应该大于generateData()消耗时间,但应该小于实际缓存对象的TTL值.
Redis正常的read-through cache示例:
$redis = new Redis();
$redis->connect(‘127.0.0.1‘, 6379);
$data = $redis->get(‘hot_items‘);
if ($data === false) {
// calculate hot items from mysql, Says: it takes 10 seconds for this process
$data = expensive_database_call();
// store the data with a 10 minute expiration
$redis->setex("hot_items", 600, $data);
}
var_dump($data);
跟Memcached缓存一样,高并发情况下Redis缓存失效时也可能会引发Dogpile效应.
下面是Redis通过使用”锁”的方式来避免Dogpile效应示例:
$redis = new Redis();
$redis->connect(‘127.0.0.1‘);
$expiry = 600; // cached 600s
$recalculated_at = 100; // 100s left
$lock_length = 20; // lock-key expiry 20s
$data = $redis->get("hot_items");
$ttl = $redis->get("hot_items");
if ($ttl <= $recalculated_at && $redis->setnx(‘lock:hot_items‘, true)) {
$redis->expire(‘lock:hot_items‘, $lock_length);
$data = expensive_database_call();
$redis->setex(‘hot_items‘, $expiry, $data);
}
var_dump($data);
上面的流程是这样的:
$redis->setnx(‘lock:hot_items‘, true)尝试创建一个key作为”锁”.若key已存在,setnx不会做任何动作且返回值为false,所以只有一个客户端会返回true值进入if语句更新缓存.原文:http://www.cnblogs.com/longhao/p/4230566.html