高并发访问下避免对象缓存失效引发Dogpile效应

避免Redis/Memcached缓存失效引发Dogpile效应

Redis/Memcached高并发访问下的缓存失效时可能产生Dogpile效应(Cache Stampede效应).

避免Memcached缓存的Dogpile效应

Memcached的read-through cache流程:客户端读取缓存,没有的话就由客户端生成缓存.
Memcached缓存示例:

复制代码
 1 $mc = new Memcached();
 2 $mc->addServers(array(
 3 array('127.0.0.1', 11211, 40),
 4 array('127.0.0.1', 11212, 30),
 5 array('127.0.0.1', 11213, 30)
 6 ));
 7 $data = $mc->get('cached_key');
 8 if ($mc->getResultCode() === Memcached::RES_NOTFOUND) {
 9 $data = generateData(); // long-running process
10 $mc->set('cached_key', $data, time() + 30);
11 }
12 var_dump($data);
复制代码

 


假如上面的generateData()是耗时3秒(或更长时间)的运算或数据库操作.当缓存服务器不可用(比如:缓存实例宕机,或网络原因)或是缓存失效瞬间,如果恰好有大量的访问请求,那就会出现机器CPU消耗或数据库操作次数短时间内急剧攀升,可能会引发数据库/Web服务器故障.

避免这样的Dogpile效应,通常有两种方法:

使用独立的更新进程
使用独立的进程(比如:cron job)去更新缓存,而不是让web服务器即时更新数据缓存.举个例子:一个数据统计需要每五分钟更新一次(但是每次计算过程耗时1分钟),那么可以使用cron job去计算这个数据,并更新缓存.这样的话,数据永远都会存在,即使不存在也不用担心产生dogpile效应,因为客户端没有更新缓存的操作.这种方法适合不需要即时运算的全局数据.但对用户对象,朋友列表,评论之类的就不太适用.
使用”锁”
除了使用独立的更新进程之外,我们也可以通过加”锁”,每次只允许一个客户端请求去更新缓存,以避免Dogpile效应.
处理过程大概是这样的:
A请求的缓存没命中
A请求”锁住”缓存key
B请求的缓存没命中
B请求需要等待直到”锁”释放
A请求完成,并且释放”锁”
B请求缓存命中(由于A的运算)
Memcached使用”锁”的示例:

复制代码
 1 function get($key) {
 2 global $mc;
 3 
 4 $data = $mc->get($key);
 5 // check if cache exists
 6 if ($mc->getResultCode() === Memcached::RES_SUCCESS) {
 7 return $data;
 8 }
 9 
10 // add locking
11 $mc->add('lock:' . $key, 'locked', 20);
12 if ($mc->getResultCode() === Memcached::RES_SUCCESS) {
13 $data = generateData();
14 $mc->set($key, $data, 30);
15 } else {
16 while(1) {
17 usleep(500000);
18 $data = $mc->get($key);
19 if ($data !== false){
20 break;
21 }
22 }
23 }
24 return $data;
25 }
26 
27 $data = get('cached_key');
28 
29 var_dump($data);
复制代码

 

上面的处理方法有个缺陷,就是缓存失效时,所有请求都需要等待某个请求完成缓存更新,那样无疑会增加服务器的压力.
如果能在数据失效之前的一段时间触发缓存更新,或者缓存失效时只返回相应状态让客户端根据返回状态自行处理,那样会相对比较好.

下面的get方法就是返回相应状态由客户端处理:

复制代码
 1 class Cache {
 2 const RES_SUCCESS = 0;
 3 const GenerateData = 1;
 4 const NotFound = 2;
 5 
 6 public function __construct($memcached) {
 7 $this->mc = $memcached;
 8 }
 9 
10 public function get($key) {
11 
12 $data = $this->mc->get($key);
13 // check if cache exists
14 if ($this->mc->getResultCode() === Memcached::RES_SUCCESS) {
15 $this->_setResultCode(Cache::RES_SUCCESS);
16 return $data;
17 }
18 
19 // add locking
20 $this->mc->add('lock:' . $key, 'locked', 20);
21 if ($this->mc->getResultCode() === Memcached::RES_SUCCESS) {
22 $this->_setResultCode(Cache::GenerateData);
23 return false;
24 }
25 $this->_setResultCode(Cache::NotFound);
26 return false;
27 }
28 
29 private function _setResultCode($code){
30 $this->code = $code;
31 }
32 
33 public function getResultCode(){
34 return $this->code;
35 }
36 
37 public function set($key, $data, $expiry){
38 $this->mc->set($key, $data, $expiry);
39 }
40 }
41 
42 $cache = new Cache($mc);
43 $data = $cache->get('cached_key');
44 
45 switch($cache->getResultCode()){
46 case Cache::RES_SUCCESS:
47 // ...
48 break;
49 case Cache::GenerateData:
50 // generate data ...
51 $cache->set('cached_key', generateData(), 30);
52 break;
53 case Cache::NotFound:
54 // not found ...
55 break;
56 }
复制代码

 

上面的memcached缓存失效时,只有一个客户端请求会返回Cache::GenerateData状态,其它的都会返回Cache::NotFound.客户端可通过检测这些状态做相应的处理.
需要注意的是:”锁”的TTL值应该大于generateData()消耗时间,但应该小于实际缓存对象的TTL值.

避免Redis缓存的Dogpile效应

Redis正常的read-through cache示例:

复制代码
 1 $redis = new Redis();
 2 $redis->connect('127.0.0.1', 6379);
 3 
 4 $data = $redis->get('hot_items');
 5 
 6 if ($data === false) {
 7 // calculate hot items from mysql, Says: it takes 10 seconds for this process
 8 $data = expensive_database_call();
 9 // store the data with a 10 minute expiration
10 $redis->setex("hot_items", 600, $data);
11 }
12 var_dump($data);
复制代码

 

跟Memcached缓存一样,高并发情况下Redis缓存失效时也可能会引发Dogpile效应.
下面是Redis通过使用”锁”的方式来避免Dogpile效应示例:

复制代码
 1 $redis = new Redis();
 2 $redis->connect('127.0.0.1');
 3 
 4 $expiry = 600; // cached 600s
 5 $recalculated_at = 100; // 100s left
 6 $lock_length = 20; // lock-key expiry 20s
 7 
 8 $data = $redis->get("hot_items");
 9 $ttl = $redis->get("hot_items");
10 
11 if ($ttl <= $recalculated_at && $redis->setnx('lock:hot_items', true)) {
12 $redis->expire('lock:hot_items', $lock_length);
13 $data = expensive_database_call();
14 $redis->setex('hot_items', $expiry, $data);
15 }
16 var_dump($data);
复制代码

 

上面的流程是这样的:

正常获取key为hot_items的缓存数据,同时也获取TTL(距离过期的剩余时间)
上面hot_items过期时间设置为600s,但当hot_items的TTL<=100s时,就触发缓存的更新过程
$redis->setnx('lock:hot_items', true)尝试创建一个key作为”锁”.若key已存在,setnx不会做任何动作且返回值为false,所以只有一个客户端会返回true值进入if语句更新缓存.
给作为”锁”的key设置20s的过期时间,以防PHP进程崩溃或处理过期时,在作为”锁”的key过期之后允许另外的进程去更新缓存.
if语句中调用expensive_database_call(),将最新的数据正常保存到hot_items.

原文地址:aHR0cDovL3d3dy56cndtLmNvbS8/cD03MjY1 (BASE64)

博主注:
这篇文章中的方法是一个很好的方法:
A请求的缓存没命中
A请求”锁住”缓存key
B请求的缓存没命中
B请求需要等待直到”锁”释放
A请求完成,并且释放”锁”
B请求缓存命中(由于A的运算)
但实现过程中有一个细节可以进行改进,就是当缓存命中时,不需要等过期再执行以上操作,只需在一定阈值范围内,直接在读取值之前,由当前请求将该命中的缓存更新掉就行。这样就不会碰到缓存一过期,有非常多请求加锁等待的情况了。

posted on   咚..咚  阅读(917)  评论(0编辑  收藏  举报

编辑推荐:
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

导航

统计

点击右上角即可分享
微信分享提示