这篇文章主要讨论的问题是:如何为项目设计一个完整而简洁的缓存系统。只讲做法,不讲原理。在我们项目中,使用到了三种方法,来保证了缓存系统的有效简洁。
普通缓存
1)先从缓存中获取数据(如果在缓存中获取到,则直接返回已获取的数据)
2)如果获取不到,再从数据库里面读取相应的数据
3) 把获取到的数据加入缓存中
注意:这种方式是在Model层,也就是业务处理层加入的。
实例代码如下:
public static function getCombatPowerRank()
{
$cacheKey = 'Rank:CombatPower';
// 先从缓存中读取
if ($list = F('Memcache')->get($cacheKey)) {
return $list;
}
$list = array();
// 遍历所有用户分库,执行清理
for ($i = 1; $i <= DIST_USER_DB_NUM; $i++) {
if ($distList = Dao('Dist_User')->setDs($i)->getCombatPowerTopUsers(self::RANK_LIMIT)) {
$list = array_merge($list, $distList);
}
}
// 保存到缓存中
F('Memcache')->set($cacheKey, $list, C('RANK_CACHE_TIME'));
return $list;
}
这种方式确实很好理解,有一个弊端就是,所有的缓存都需要手动的加上以上缓存的代码,需要修改函数的内部代码。请注意,我们在项目中加入缓存的时间是项目完成的差不多了,也就是说需要有很多这样的“读取类”函数加入缓存,如果全是以上这种加入缓存方式的话,需要修改很多函数的内部代码,那绝对是一个复杂而容易遗漏的苦力活。如果一不小心,就会出现错误。有没有好的方式可以集中的给某些函数加入这样的缓存系统呢(如果有的话,绝对是一个福音,哈哈)
静态资源缓存
在解释这种方法之前,我先简要说明一下我们的需求,便于更好理解为什么我可以这么做。
在我们的游戏项目中,有一部分数据时静态资源数据,这种数据时配置好的,不会经常变动,每个用户需要的都一样。所以当调用一次这个方法后,就把这个方法的发挥着缓存起来,下次在调用这个方法的时候,就可以直接读取缓存。
不改变方法的内部代码,却可以给每个方法加入缓存,PHP魔术方法__call()就可以实现,如果对象调用某个方法,而这个方法又不存在,那么就会调用到这个魔术方法了,具体实现代码如下:
/**
* 调用魔术方法
*
* @param string $method
* @param mixed $args
* @return mixed
*/
public function __call($method, $args)
{
if (! method_exists($this, '__CACHE__' . $method)) {
// 这里是实现数据库链式查询的,这里可以忽略
return parent::__call($method, $args);
}
$cacheKey = md5($this->_dbName . ':' . $this->_tableName . ':' . $method . ':' . serialize($args));
$data = $this->_cache->get($cacheKey);
if ($data === false) {
// 调用这个类里面的方法
$data = call_user_func_array(array($this, '__CACHE__' . $method), $args);
// 把这个函数的返回结果缓存起来
$this->_cache->set($cacheKey, $data);
}
return $data;
}
代码运行机制: 比如说有这样的一个调用关系:Dao('Static_Ship')->get(),但是在Static_Ship这个类中没有get()这个方法,于是程序就会执行__call(),在这个类中,有一个这样的方法__CACHE__get()这样的一个方法,于是我就执行了这个方法,并且把这个函数的数据缓存起来了。这样就达到了我们的目的,不改变函数内部的代码,把函数的结果缓存起来。
这里我并没有实现静态资源缓存的更新,如果配置资源变动了,我只有清除保存这个配置资源的memcache服务器的所有数据。
单条记录主键缓存
如果大家细心的话,可以发现方法2中缓存的键值设计并不针对某一个用户:$cacheKey = md5($this->_dbName . ':' . $this->_tableName . ':' . $method . ':' . serialize($args)); 缓存值加入库名,是因为可能有多个数据库。
如果需要缓存的数据和用户有关系,怎么缓存读取用户的单条信息,比如我的某一条船的信息。注意和用户有关的信息会经常变动的,需要特别注意缓存的更新。当删除,更新用户数据的时候,都需要自动更新缓存。
在我们项目中,需要读取“我的船”相应的数据。比如
1)我需要读取我的船的攻击力:getShipFieldByUserShipId($uid, $shipId, attack)
2) 我需要读取船的防御力 :getShipFieldByUserShipId($uid, $shipId, defence)
3) 读取我的船的航海速度:getShipFieldByUserShipId($uid, $shipId, speed)
这个时候,有两种SQL查询方法:
1) $data = {userShipId = userShipId. AND field=$field}
2) $data = {userShipId = userShipId} 然后再这个$data数组中,返回相应的$data[$field].
你可能会觉得第二种方法会获取到一些无用的数据,不好。但是,事实上,第二种方法比第一种方法好,因为他可以使用索引查询,这个属于SQL优化的,暂且不讨论。
第二个原因是便于方法可以加入缓存,查询条件越“统一”,越容易加入缓存,这里的做法是使用主键缓存。userShipId就是主键,根据这个主键缓存我的一条记录。这个也是Dao层处理。
public function getField($pk, $field)
{
// 禁用缓存时
if (! $this->_isCached) {
return $this->field($field)
->where($this->_getPkCondition($pk))
->fetchOne();
}
$data = $this->get($pk);
return isset($data[$field]) ? $data[$field] : null;
}
get方法的主要代码如下:
/**
* 根据主键 fetchRow
*
* @param mixed $pk
* @return array
*/
public function get($pk)
{
// 禁用缓存时
if (! $this->_isCached) {
return $this->where($this->_getPkCondition($pk))->fetchRow();
}
$cacheKey = $this->_getRowCacheKey($pk);
// 保证相同的静态记录只读取一遍
if (isset($this->_rowDatas[$cacheKey])) {
return $this->_rowDatas[$cacheKey];
}
$row = $this->_cache->get($cacheKey);
if ($row === false) {
$row = $this->where($this->_getPkCondition($pk))->fetchRow() ?: array();
$this->_cache->set($cacheKey, $row, $this->_cacheTTL);
$this->_rowDatas[$cacheKey] = $row;
}
return $row;
}
使用主键作为缓存键值,获取缓存键值的方法如下,注意联合主键的情况:
// 获取单条记录缓存key
protected function _getRowCacheKey($pk)
{
if (is_array($pk)) {
$pkString = implode(':', $pk);
}
else {
$pkString = $pk;
}
return md5($this->_dbName . ':' . $this->_tableName . ':get:' . $pkString);
}
更新我的数据的时候,比如删除我的某一条记录,必须需要根据主键来删除,这样可以自动删除缓存:
/**
* 删除(根据主键)
*
* @param mixed $pk
* @param array $extraWhere 格外的WHERE条件
* @return bool
*/
public function deleteByPk($pk, array $extraWhere = array())
{
$where = $this->_getPkCondition($pk);
if ($extraWhere) {
$where = array_merge($where, $extraWhere);
}
if (! $result = $this->where($where)->delete()) {
return $result;
}
// 清理缓存
if ($this->_isCached) {
$this->_deleteRowCache($pk);
}
// 统计Memcache读写次数
Dao('Massive_MemcacheRecord')->mark($this->_dbName, $this->_tableName, __METHOD__, 1);
return $result;
}
缓存系统需要注意的几点:
1) 注意缓存系统的关联性,如果数据发生了变化,一定要更新缓存
2)如果被缓存的数据和用户有关,一定要把$cacheKey处理好,保证每个用户数据不会被其它用户串改。特别需要注意的是分库的时候uid=1可不止一个哦
3)如果有必要的话,可以做一个缓存命中率的统计,统计哪些库的那些表被哪些函数操作的次数
4) 如果某些表的数据频繁的被修改,可以不需要缓存,如果用户的行文记录表,_isCached 这个属性就是用来控制是否需要缓存。
见如下代码: