想到站点性能的提升, 我们条件反射的会想到缓存, 想到缓存绝大部分技术人员会想到memcached或者redis.
确实, memcached和redis还有很多其它类似NoSQL确实是高效,可伸缩的缓存解决方案.
这里谈论的是轻量级的文件缓存的实现, 很多情况下他比NoSQL缓存能够更好的帮助我们解决问题.
一. 看一个测试结果:
有一个数据量10W左右的电子商务平台, 当时的日访问IP为5W左右.
我对其中的一个活动页面做了压力测试:(有很多的数据库查询, 服务器配置: WinNT/IIS/2G/2.4GHZ 双核)
1. 裸跑: 320rps (取的平均值)
2. 使用有效的文件缓存: 510rps (取平均值)
3. memcached: 560rps (取平均值)
对于当前这个站点, 你会选用那种缓存方案???
redis还有memcached的总体缓存速度并不比文件缓存快很多, 他们的优势是可以分布式或者集群, 并发数和响应速度的导数函数比文件缓存更让人满意.
同时也说明了某些情况下文件缓存确实能够帮助我们更好的解决问题, 而且它是廉价的.
二. 文件缓存的设计和实现:
1. 确定缓存数据:
缓存: 将需要经过一个过程的计算才能得到的结果存储下来, 在一定的时间内使用存储下来的结果来避免过程的执行, 从而提高过程的性能. (当然存储结果的获取需要能够快速响应)
所以我们的第一步就是要确定你需要缓存的结果, 当然这个属于缓存的应用范畴, 但是事先考虑这个问题有利于我们的设计和实现:
(1). 一个数据库查询结果.
(2). 一个函数的计算结果.
(3). 一个页面的执行结果.
......
2. 确定存储介质:
得要要缓存的数据之后, 接下来的重点是确定将结果缓存到哪种介质.
(1). 磁盘
(2). 内存
撇开成本不考虑, 如果有足够的内存, 那内存是不二的选择, 访问速度是磁盘的100W倍. 但是, 大部分情况下我们没有那么多的可用内存.
通常的DBMS或者文件是第一中存储介质, 而redis和memcached等内存数据库是将数据存储在内存中.(这里抛开它们的持久化功能)
这里探讨的是文件缓存, 已经确定将存储介质固定为磁盘.
3. 缓存的管理:
这个是缓存核心也是难点, 管理包括: 缓存的快速获取和存储以及过期缓存内容的自动清理.
在此我们可以将redis或者memcached理解为缓存管理工具, 他们提供了接口用于存储和获取缓存数据, 并且会自动的管理缓存的内容.
而对于文件缓存, 完全是自己实现, 所以这里的所有步骤都需要我们自己去处理. 这里提供两种方式来组织管理缓存内容:
(1). 简易DBMS方式:
类似于MySQL的MyISAM引擎, 给定一个索引文件和n个数据文件.
存储: 先写入数据文件, 并且记下数据索引信息, 然后再写入索引文件. (最少两次磁盘操作).
获取: 依据条件查询索引文件, 得到数据索引信息, 再快速定位并且获取数据. (最少两次磁盘操作, 而且还需要看索引文件所使用的数据结构)
优点: 多条数据存储在一个数据文件中, 可以有效的控制缓存文件的数量, 可以应对不断增长的数据量, 缓存的效率依赖与实现程序.
缺点: 数据量偏大时, 会影响缓存写入和查询的效率, 为了保证效率, 这种系统通常使用惰性删除和更新, 这样索引文件和数据文件会膨胀的很快.
实现: 对于php来说, bdb,gdb等扩展是不错的选择.
(2). 一结果一文件方式:
也就是一个缓存结果一个文件, 这样子缓存的写入和获取就变成了纯粹的IO操作, 性能依赖与文件系统.
存储: 直接写入指定文件.
获取: 直接获取指定文件内容.
优点: 数据直接获取, 速度比bdb方式快. (一次磁盘索引查询时间和 ( size/(2^14) - size/(2^11) ) 次数据读取时间).
缺点: 文件数量会快速的不断的膨胀, 当数据偏多的时候就会影响文件的查找速度.
实现: 后面会给出方式的实现.
这两种方式都可以作为缓存, 第一种方式需要安装php扩展(如果可以安装扩展, 那么apc也是一个不错的选择), 第二种方式使用php自己就可以很好的实现. (php的IO操作性能还是不错的)
这里选用第二中方式: 一个结果一个文件.
4. 考虑不断增长的数据:
如果是使用memcached或者redis, 他们是基于tcp/IP的单独运行的服务器, "一致性hash算法"可以很好的适应不断新增的新机器, 也就实现了无限制扩展, 解决了不断增长的数据量. 哈, 网络本来就是最好的分布式系统.
如果使用上面提到的文件组织方式又该如何管理这些文件呢?
这也是一种处理大数据的方式: 分区, 对于文件来说就是分文件夹存储.
(1). 垂直分区: 固定文件夹数量n, 然后将文件散列到n个文件夹中.
起初我们需要估计下数据量来确定文件夹的数量, 例如: 是用1000个小文件夹来存储100W的数据文件, 接近每个文件夹内放置100W/1000 = 1000(个)文件. 当然这个是理想情况下, 事实任何的hash函数的都无法保证实际中的这个效果.
(2). 水平分区: 固定每个文件夹中存放的数据文件数, 然后让文件夹不断的增多.
起初我们也必须确定大概的数据量: 例如估计缓存数据文件数量大概: 100W, 确定每个子文件夹中存放1000个数据文件. 也就是: 0-999的数据文件存放在0这个子文件夹中, 1000-1999的数据文件存放在1这个文件夹中. 依此类推...
(3). 我们可以做的更好:
例如: 有一个文章列表页面, 进行了一级分类, 也就是通常我们要访问这个页面需要两个参数:
1). tid 类别Id, 2). pageno 当前要查看页码.
在这个情况下, 我们可以先依据tid来分文件夹, 然后再依据pageno来做水平分区, 这样可以达到更好的效果. 也就是缓存管理工具最好能够提供接口让开发者能够操作分区.
依据实际情况, 我们选择第三中方式.
5. 一种简洁高效的实现:
(1). 确定接口:
从我们的需求我们需要缓存管理工具提供两个接口:
写入缓存: set(_key, _value);
获取缓存: get(_key, _value);
<?php /** * dynamic content cache common interface . */ interface ICache { public function get( $_baseId, $_factor, $_time ); public function set( $_baseId, $_factor, $_content ); } ?>
(2). 依据上面提供的管理方式实现接口.
我们可以做到和redis以及memcached缓存类的兼容, 从而将缓存系统设计为工厂模式. 毕竟对于开发着来说, 缓存系统重要的是接口, redis和文件缓存不同在于存储介质, 它们需要对外提供相同的接口.
(3). 对于php尽量使用系统函数来帮助完成任务, 确保缓存的写入和获取简单化.
一种简洁的实现: (也是产品中用到的工具)
<?php /** * dynmaic content file cache class. */ class FileCache implements ICache { private $_length = 3000; public $_cache_dir = NULL; public function __construct( $_args ) { if ( $_args != NULL ) { if ( isset($_args['cache_dir']) ) $this->_cache_dir = $_args['cache_dir']; if ( isset($_args['length']) ) $this->_length = $_args['length']; } } private function getCacheFile($_baseId, $_factor) { $path = $this->_cache_dir.str_replace('.', '/', $_baseId); if ( $_factor != NULL ) { $path = $path.'/'.floor(($_factor / $this->_length)); $_file = ($_factor % $this->_length).'.cache.html'; } else { $_file = 'default.cache.html'; } return ($path.'/'.$_file); } public function get( $_baseId, $_factor, $_time ) { $_cache_file = $this->getCacheFile($_baseId, $_factor); //echo $_cache_file,'<br />'; if ( ! file_exists( $_cache_file ) ) return FALSE; if ( $_time < 0 ) return file_get_contents($_cache_file); if ( filemtime( $_cache_file ) + $_time < time() ) return FALSE; return file_get_contents($_cache_file); //return $_cache_file; } public function set( $_baseId, $_factor = NULL, $_content ) { $_cache_file = $this->getCacheFile($_baseId, $_factor); $path = dirname($_cache_file); //check and make the $path dir if ( ! file_exists( $path ) ) { $_dir = dirname( $path ); $_names = array(); do { if ( file_exists( $_dir ) ) break; $_names[] = basename($_dir); $_dir = dirname( $_dir ); } while ( true ); for ( $i = count($_names) - 1; $i >= 0; $i-- ) { $_dir = $_dir.'/'.$_names[$i]; mkdir($_dir, 0x777); } mkdir($path, 0x777); } return file_put_contents($_cache_file, $_content); } } ?>
我们的应用场景是将整个页面的执行结果缓存下来, 所以缓存文件后缀用了".html". 对于有安全需要的系统, 尽量保持缓存文件后缀为".php"
一个文件列表页面的调用方法:
该缓存类会依据$_key中的"."来自动分区. 例如下面的例子就会生产$_cache_dir/article/list/$_tid/这个文件夹, 在这个文件夹下再依据pageno水平分区.
<?php //$_tid为查看的类别Id //pageno为当前查看的页码 $_cache = CacheFactory::create('file', array('cache_dir'=>'缓存根目录')); $_key = 'article.list.'.$_tid; $_ret = $_cache->get($_key, $_pageno, 3600); if ( $_ret != FALSE ) { echo $_ret; exit(); } //生成缓存 $_cache->set($_key, $_pageno, 3600); ?>
这里将缓存系统设计为了工厂模式, 可以兼容redis和memcached, 也就是系统可以很方便的将缓存介质从文件转移到内存缓存. CacheFactory源码:
<?php /** * dynamic content cache factory . * * @author chenxin <[email protected]> */ define('_CACHE_HOME_', dirname(__FILE__)); class CacheFactory { private static $_classes = NULL; public static function create( $_class, $_args = NULL ) { if ( self::$_classes == NULL ) { self::$_classes = array(); //require the common interface require _CACHE_HOME_.'/ICache.class.php'; } $_class = ucfirst( $_class ).'Cache'; if ( ! isset( self::$_classes[$_class] ) ) { require _CACHE_HOME_.'/'.$_class.'.class.php'; self::$_classes[$_class] = true; } //return the newly created object return new $_class($_args); } } ?>