使用memcache处理缓存的三种方案
这篇文章主要讨论的问题是:如何为项目设计一个完整而简洁的缓存系统。只讲做法,不讲原理。在我们项目中,使用到了三种方法,来保证了缓存系统的有效简洁。
1) 第一种,最常见的方式 读取数据的主要步骤如下:
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;
- }
这种方式确实很好理解,有一个弊端就是,所有的缓存都需要手动的加上以上缓存的代码,需要修改函数的内部代码。请注意,我们在项目中加入缓存的时间是项目完成的差不多了,也就是说需要有很多这样的“读取类”函数加入缓存,如果全是以上这种加入缓存方式的话,需要修改很多函数的内部代码,那绝对是一个复杂而容易遗漏的苦力活。如果一不小心,就会出现错误。有没有好的方式可以集中的给某些函数加入这样的缓存系统呢(如果有的话,绝对是一个福音,哈哈)
2)第二种方式 ,在DAO层集中处理。在解释这种方法之前,我先简要说明一下我们的需求,便于更好理解为什么我可以这么做。
在我们的游戏项目中,有一部分数据时静态资源数据,这种数据时配置好的,不会经常变动,每个用户需要的都一样。例如各种角色类的基础的属性,船只的基础属性等。这类数据涉及到的操作一般是读:把一张表全部读出来,获取根据某个条件读取相应的内容。既然操作单一,我们就直接在DAO层处理这类方法的缓存。做法就是给每一个Dao类里面的函数加入缓存。
不改变方法的内部代码,却可以给每个方法加入缓存,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()这样的一个方法,于是我就执行了这个方法,并且把这个函数的数据缓存起来了。这样就达到了我们的目的,不改变函数内部的代码,把函数的结果缓存起来。
3)集中处理和用户有关的数据的缓存。如果大家细心的话,可以发现方法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) uid = $uid AND shipId = $shipId AND field=$field
2) $data = " uid = $uid AND shipId = $shipId " 然后再这个$data数组中,返回相应的$data[$field].
你可能会觉得第二种方法会获取到一些无用的数据,不好。但是,事实上,第二种方法比第一种方法好,因为他可以使用索引查询,这个属于SQL优化的,暂且不讨论,第二个原因是便于方法可以加入缓存,查询条件越“统一”,越容易加入缓存。第三种做法也是在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;
- }
获取缓存键值的方法_getRowCacheKey()实现方式如下:
- // 获取单条记录缓存key
- protected function _getRowCacheKey($pk)
- {
- if (is_array($pk)) {
- $pkString = implode(':', $pk);
- }
- else {
- $pkString = $pk;
- }
- return md5($this->_dbName . ':' . $this->_tableName . ':get:' . $pkString);
- }
保证查询条件的高度统一,根据查询的条件设置缓存,就是第三中做法的精髓了。
缓存系统需要注意的几点:
1) 注意缓存系统的关联性,如果数据发生了变化,一定要更新缓存
2)如果被缓存的数据和用户有关,一定要把$cacheKey处理好,保证每个用户数据不会被其它用户串改。特别需要注意的是分库的时候uid=1可不止一个哦
3)如果有必要的话,可以做一个缓存命中率的统计,统计哪些库的那些表被哪些函数操作的次数
4) 如果某些表的数据频繁的被修改,可以不需要缓存,如果用户的行文记录表,_isCached 这个属性就是用来控制是否需要缓存。
见如下代码:
- /**
- * 删除(根据主键)
- *
- * @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;
- }
使用memcache处理缓存的三种方案的更多相关文章
- Spring-Boot-操作-Redis,三种方案全解析!
在 Redis 出现之前,我们的缓存框架各种各样,有了 Redis ,缓存方案基本上都统一了,关于 Redis,松哥之前有一个系列教程,尚不了解 Redis 的小伙伴可以参考这个教程: Redis 教 ...
- 【Win 10 应用开发】文件读写的三种方案
本文老周就跟伙伴们探讨一下关于文件读写的方法.总得来说嘛,有三种方案可以用,而且每种方案都各有特色,也说不上哪种较好.反正你得记住老祖宗留给我们的大智慧——事无定法,灵活运用者为上. OK,咱们开始吧 ...
- SP避免Form重复提交的三种方案
SP避免Form重复提交的三种方案 1) javascript ,设置一个变量,只允许提交一次. <script language="javascript"> ...
- [Linux]三种方案在Windows系统下安装ubuntu双系统(转)
在学习linux的过程中,ubuntu无疑是初学者的最佳选择. 下面来列举给Windows系统安装ubuntu双系统的三种方法. 一.虚拟机安装(不推荐) 使用工具:Vmware 如果不是因为迫不得已 ...
- 关于Jenkins部署代码权限三种方案
关于Jenkins部署代码权限三种方案 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.修改Jenkins进程用户为root [root@jenkins ~]# cat /etc ...
- 三种方案在Windows系统下安装ubuntu双系统
一.虚拟机安装(不推荐) 使用工具:Vmware 如果不是因为迫不得已,比如Mac OS对硬件不兼容,Federa安装频繁出错,各种驱动不全等等,不推荐使用虚拟机安装. 个人感觉这是一种对操作系统的亵 ...
- MySQL冗余数据的三种方案
一,为什么要冗余数据 互联网数据量很大的业务场景,往往数据库需要进行水平切分来降低单库数据量. 水平切分会有一个patition key,通过patition key的查询能够直接定位到库,但是非pa ...
- [SQL]用于提取组内最新数据,左连接,内连接,not exist三种方案中,到底谁最快?
本作代码下载:https://files.cnblogs.com/files/xiandedanteng/LeftInnerNotExist20191222.rar 人们总是喜欢给出或是得到一个简单明 ...
- mongo数据同步的三种方案
(一)直接复制data目录(需要停止源和目标的mongo服务)1.针对目标mongo服务已经存在,并正在运行的(mongo2-->mongo).执行步骤:(1).停止源/目标服务器的mongo服 ...
随机推荐
- Maven常用指令
Maven是通过pom.xml配置好一系列操作,然后通过命令行来进行控制操作的.格式为 mvn 命令 常用命令主要有: mvn 命令 -v:查看maven版本 compile:编译项目 test:测试 ...
- Java多线程之线程的状态以及线程间协作通信导致的线程状态转换
转载请注明原文地址:http://www.cnblogs.com/ygj0930/p/6561589.html 一:线程的状态以及变化图 Java中线程中状态可分为五种:New(新建状态),Ru ...
- vs2017莫名自动退出调试状态可以尝试一下如下的方法
- 索引全扫描(INDEX FULL SCAN)
所谓的索引全扫描(INDEX FULL SCAN)就是指要扫描目标索引所有叶子块的所有索引行.这里需要注意的是,索引全扫描需要扫描目标索引的所有叶子块,但这并不意味着需要扫描该索引的所有分支块.在默认 ...
- Java程序监控指标
监控指标: 1.CPU平均使用率 2.内存平均使用率 3.应用程序错误数 4.应用程序请求量 5.应用平均响应时间 6.硬件I/O指标 7.JMX 7.1.Full gc count 7.2.Full ...
- Java 线程池之FixedThreadPool(Java代码实战-003)
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util ...
- Java之所有对象的公用方法>9.Always override hashCode when you override equals
You must override hashCode in every class that overrides equals.
- C#和JAVA的RSA密钥、公钥转换
C#的秘钥跟JAVA的密钥区别 RSA对于程序本身是没有区别的,其格式都是相同的.对于不同的程序来说,存储使用的语法(包装的类)会有所不同. RSA语法和语法标准有很多,大的类型大概分为ASN.1 ...
- (转载)【TP5.0】设置session有效时长+修改默认存储路径
//查看默认session存储路径:print_r(session_save_path()); \thinkphp\helper.php if (!function_exists('ses ...
- Easyui入门视频教程 第02集--- ASP.NET MVC下 搭建 EasyUI环境
Easyui入门视频教程 第02集--- ASP.NET MVC下 搭建 EasyUI环境 目录 ----------------------- Easyui入门视频教程 第09集---登录完善 图标 ...