文档章节

php动态内容文件缓存的设计和高效实现

狮子的魂
 狮子的魂
发布于 2013/08/29 14:32
字数 2535
阅读 1125
收藏 9

想到站点性能的提升, 我们条件反射的会想到缓存, 想到缓存绝大部分技术人员会想到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 <chenxin619315@gmail.com>
*/
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);
     }
}
?>

© 著作权归作者所有

共有 人打赏支持
狮子的魂

狮子的魂

粉丝 205
博文 11
码字总数 11922
作品 7
深圳
CEO
私信 提问
加载中

评论(2)

狮子的魂
狮子的魂

引用来自“Koma”的评论

对于简易DBMS方法中的那个“惰性删除和更新”应该怎么实现?
下面那个一结果一文件中get和set都需要提供缓存过期时间,有一种方法可以只在set的时候设置过期时间而在get的时候就不需要了,这个应该好些吧,不知道有什么方法可以很好的实现?我只能想到就是在set的时候在数据文件的第一行保存缓存过期时间,然后下一行之后存储真正的数据,那get的时候就先读取这个文件并判断时间,过期就删掉当前文件否则直接读取。

惰性删除使用一个标记就可以了, 然后将标记删除的空间索引存储起来, 方便下次写入或者更新利用.
get提供时间就可以了, 这种缓存方式的过期管理是在逻辑层, 不益于放在底层类库...
Koma
Koma
对于简易DBMS方法中的那个“惰性删除和更新”应该怎么实现?
下面那个一结果一文件中get和set都需要提供缓存过期时间,有一种方法可以只在set的时候设置过期时间而在get的时候就不需要了,这个应该好些吧,不知道有什么方法可以很好的实现?我只能想到就是在set的时候在数据文件的第一行保存缓存过期时间,然后下一行之后存储真正的数据,那get的时候就先读取这个文件并判断时间,过期就删掉当前文件否则直接读取。
迈思内容管理系统--MyStepCMS

总体介绍 迈思网站内容管理系统(MyStepCMS)是基于 Data->PHP->Template 模式,采用网络中已经成熟、稳定的技术PHP+MYSQL开发而成,利用本系统您可以很方便地搭建并管理自己的网站。 MyStep...

windy2000
2012/08/10
2.4K
0
LAMP网站架构分析

转自:http://www.williamlong.info/archives/1908.html LAMP(Linux-Apache-MySQL-PHP)网站架构是目前国际流行的Web框架,该框架包括:Linux操作系统,Apache网 络服务器,MySQL数据库,P...

长征4号
2017/07/05
0
0
EaglePHP v1.8 更新日志

EaglePHP,是一款开源、高效、面向对象的PHP MVC开发框架,完全基于PHP5可用于开发WEB程序和服务,借鉴国外优秀框架的设计思路,分层的设计思想使独立开发成为可能,建立模型推动代码的重 用...

oschina
2012/06/10
645
0
云端开源⾼性能技术架构调研分析报告

并发总是网站架构最大的挑战之一。由于 web 服务的兴起,并发 的数量级在不断增长。热门网站为几十万甚至几百万的同时在线用户 提供服务并不寻常。十年前,并发的主要原因是由于客户端接入速...

dukeke
2015/07/10
0
0
PbootCMS V1.3.0 发布,站群功能重磅来袭

PbootCMS V1.3.0 build 2018-11-12 1、优化会话创建机制,避免会话文件过多问题; 2、修复一些可能导致注入的安全问题(感谢topsec(zhan_ran)及CNVD的反馈); 3、修复搜索结果无法按预期显示的...

hnxsh
2018/11/12
889
3

没有更多内容

加载失败,请刷新页面

加载更多

Vert.x系列(二)--EventBusImpl源码分析

前言:Vert.x 实现了2种完成不同的eventBus: EventBusImpl(A local event bus implementation)和 它的子类 ClusteredEventBus(An event bus implementation that clusters with other Ve......

冷基
51分钟前
1
0
Perl - 获取文件项目

参考:http://www.runoob.com/perl/perl-directories.html 下面返回JSON格式的文件列表 #!/usr/bin/perluse strict;use warnings;use utf8;use feature ':5.26';require Fi......

wffger
昨天
2
0
vue组件系列3、查询下载

直接源码,虽然样式样式不好看,逻辑也不是最优,但是可以留作纪念。毕竟以后类似的功能只需要优化就可以了,不用每次都重头开始。。。 <template> <div class="pre_upload"> <div ...

轻轻的往前走
昨天
2
0
java浅复制和深复制

之前写了数组的复制,所以这里继续总结一下浅复制和深复制。 浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝。 深拷贝:对基本数据类型进行值传递,对引用数据类型,...

woshixin
昨天
2
0
kubernetes 二进制包安装

环境 角色 主机名 内网 IP 集群 IP 操作系统 服务 执行目录 部署机 k8s-master master120 10.0.4.120 - CentOS kube-apiserver kube-scheduler kube-controller-manager /opt/kubernetes/ et......

Colben
昨天
10
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部