Memcache的mutex设计模式 -- 高并发解决方案
场景
Mutex主要用于有大量并发访问并存在cache过期的场合,如
- 首页top 10, 由数据库加载到memcache缓存n分钟;
- 微博中名人的content cache, 一旦不存在会大量请求不能命中并加载数据库;
- 需要执行多个IO操作生成的数据存在cache中, 比如查询db多次;
问题
在大并发的场合,当cache失效时,大量并发同时取不到cache,会同一瞬间去访问db并回设cache,可能会给系统带来潜在的超负荷风险。我们曾经在线上系统出现过类似故障。
解决方法
方法一
高并发时,增加data_lock信号标识,只充许一个用户update cache,在cache数据期间,其它并发用户等待,只到这个用户cache成功,当然这个地方需要设置最大等待时间,毕竟当很长时间cache不成功时,不能让用户一直等待:
<?php function get_my_data2(){ $cache_id = "mykey"; $data = $memcache_obj->get($cache_id); if (!$data) { // check to see if someone has already set the lock $data_lock = $memcache_obj->get($cache_id . '_qry_lock'); if ($data_lock) { $lock_counter = 0; // loop until you find that the lock has been released. that implies that the query has finished while ($data_lock) { // you may only want to wait for a specified period of time. // one second is usually sufficient since your goal is to always have sub-second response time // if you query takes more than 1 second, you should consider "warming" your cached data via a cron job if ($lock_counter > $max_time_to_wait) { $lock_failed = true; break; } // you really want this to be a fraction of a second so the user waits as little as possible // for the simplicity of example, I'm using the sleep function. sleep(1); $data_lock = $memcache_obj->get($cache_id . '_qry_lock'); } // if the loop is completed, that either means the user waited for too long // or that the lock has been removed. try to get the cached data again; it should exist now $data = $memcache_obj->get($cache_id); if ($data) { return $data; } } // set a lock for 2 seconds $memcache_obj->set($cache_id . '_qry_lock', true, 2); $data = get_data_from_db_function(); $memcache_obj->set($cache_id, $data, $sec_to_cache_for); // don't forget to remove the lock $memcache_obj->delete($cache_id . '_qry_lock'); } return $data; }
方法二
需要给数据增加一个cache过期时间标识,高并发时,在用户取数据时,检查cache是否快要过期,如果即将过期,则充许一个用户去更新cache,其它用户依然访问没有update的数据。
<?php function get_my_data3() { $cache_id = "mykey"; $data = $memcache_obj->get($cache_id); // if there is cached data and the expire timestamp has already expired or is within the next 2 minutes // then we want the user to freshen up the cached data if ($data && ($data['cache_expires_timestamp'] - time()) < 120) { // if the semaphore lock has already been set, just return the data like you normally would. if ($memcache_obj->get($cache_id . '_expire_lock')) { return $data; } // now we want to set the lock and have the user freshen the data. $memcache_obj->set($cache_id . '_expire_lock', true, 2); // by unsetting the data it will cause the data gather logic below to execute. unset($data); } if (!$data) { // be sure to include all of the semaphore logic from example 2 // set the _qry_lock for 2 seconds $memcache_obj->set($cache_id . '_qry_lock', true, 2); $raw_data = get_data_from_db_function(); $data['cache_expires_timestamp'] = time() + $sec_to_cache_for; $data['cached_data'] = $raw_data; $memcache_obj->set($cache_id, $data, $sec_to_cache_for); // remove the _qry_lock $memcache_obj->delete($cache_id . '_qry_lock'); // remove the _expires_lock $memcache_obj->delete($cache_id . '_expires_lock'); } return $data; }
项目中的一个缓存类参考:
CacheModel.class.php
<?php namespace framework; use framework\Cache; /** * 缓存模型 - 业务逻辑模型 * * @example * setType($type) 主动设置缓存类型 * set($key, $value, $expire = 0) 设置缓存key=>value,expire表示有效时间,0表示永久 * get($key, $mutex = false) 获取缓存数据,支持mutex模式 * getList($prefix, $key) 批量获取指定前缀下的多个key值的缓存 * rm($key) 删除缓存 */ class CacheModel { protected $config = array(); // 缓存配置文件 protected $handler = null; // 当前缓存操作对象 protected $type = ''; // 当前缓存类型 /** * 取得缓存类实例 * * @param array $config 缓存节点 * @return mixed 返回类实例 */ public static function getInstance($connection = 'default') { static $_instance = null; if (!isset($_instance)) { $_instance = new self($connection); } return $_instance; } /** * 初始化缓存模型对象,缓存类型 * * @return void */ public function __construct($connection = '') { $this->_initHandler($connection); } /** * 初始化配置文件 */ private function _initHandler($connection = '') { // 获取缓存配置信息 $connection = $connection ? $connection : 'default'; if (!isset($this->config[$connection])) { $this->config[$connection] = array_merge(get_config('cache/__common__'), get_config('cache/' . $connection)); } $this->type = strtolower($this->config[$connection]['type']); $this->handler = Cache::getInstance($this->config[$connection]); } /** * 链式设置缓存节点 * * @param string $type 缓存类型 * @return object 缓存模型对象 */ public function setConnection($connection = '') { $this->_initHandler($connection); return $this; } /** * 设置缓存 * * @param string $key 缓存Key值 * @param mix $value 缓存Value值 * @param int $expire 有效时间(单位:秒,0表示永不过期) * @param boolean 是否设置成功 */ public function set($key, $value, $expire = 0) { $value = array( 'cache_data' => $value, // 缓存数据 'cache_mtime' => time(), // 缓存修改时间戳 'cache_expire' => is_null($expire) ? 0 : intval($expire) // 缓存有效时间 ); return $this->handler->set($key, $value); } /** * 获取缓存操作,支持mutex模式 * mutex使用注意 * 1.设置缓存(set)时,需要设置有效时间 * 2.获取缓存(get)时,需要主动创建缓存 * * @param string $key 缓存Key值 * @param boolean $mutex 是否启用mutex模式,默认启用 * @return mix 缓存数据 */ public function get($key, $mutex = true) { // 静态缓存 $sc = get_static('cache_model_' . $key); if (isset($sc)) { return $sc; } // 获取缓存数据 $data = $this->handler->get($key); // 未成功取到缓存 if ($data === false) { return false; } // 未过期 if (($data ['cache_expire'] === 0) || (($data ['cache_mtime'] + $data ['cache_expire']) > time ())) { return $this->_returnData($data['cache_data'], $key); } // 已过期 if ($mutex) { // mutex模式开启 $data['cache_mtime'] = time(); $this->handler->set($key, $data); // 返回false,让调用程序去主动更新缓存 set_static('cache_model_' . $key, null); return false; } else { // mutex模式没开启 $this->rm($key); return false; } } /** * 删除缓存 * * @param string $_key 缓存Key值 * @return boolean 是否删除成功 */ public function rm($key) { set_static('cache_model_' . $key, null); return $this->handler->rm($key); } /** * 清除缓存 * * @access public * @return boolen */ public function clear() { return $this->handler->clear(); } /** * 根据某个前缀,批量获取多个缓存 * * @param string $prefix 缓存前缀 * @param string $keys 缓存Keys值 * @return mix 缓存数据 */ public function getList($prefix = '', $keys = array()) { if ($this->type == 'memcache') { // Memcache有批量获取缓存的接口 $_data = $this->handler->getMulti($prefix, $keys); if ($_data) { foreach ($_data as $key => $val) { $data[$key] = $this->_returnData($val['cache_data'], $prefix . $key); } } } else { foreach ($keys as $key) { $_k = $prefix . $key; $data[$key] = $this->get($_k); } } return $data; } /** * 返回缓存数据操作,方法中,将数据缓存到静态缓存中 * * @param mix $data 缓存数据 * @param string $key 缓存Key值 * @return mix 缓存数据 */ private function _returnData($data, $key) { set_static('cache_model_' . $key, $data); return $data; } }
参考:
memcached PHP semaphore & cache expiration handling
[Tim]Memcache mutex设计模式