• 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 frameworkCache;
    
    /**
     * 缓存模型 - 业务逻辑模型
     *
     * @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设计模式

  • 相关阅读:
    利用MySQL实现分布式锁,涉及到乐观锁和悲观锁的思想
    SpringIOC容器的使用
    Elasticsearch 入门实战(2)安装
    ODOO升级可能遇到问题
    标记接口(Marker interface)
    将Anaconda 中新建的虚拟环境添加到Jupyter notebook
    jupyter切换kernel 连接失败,请检查配置
    解决问题:Jupyter Notebook 无法切换内核
    【c语言】截取字符串小技巧
    C#学习日记04
  • 原文地址:https://www.cnblogs.com/52php/p/5677648.html
Copyright © 2020-2023  润新知