不用Wrappers,使用Memcache原生接口实现SAE程序移植方法

    目前大多的SAE程序都是通过wrappers的方式实现移植。但SAE官方文档说,Wrappers的效率肯定不如原生接口, 建议使用原生接口。
我说一种使用原生Memcache接口实现SAE程序移植的方法,供大家参考。
我们的程序往往都会生成php缓存文件,然后include执行它。然而SAE禁止IO写操作,所以生成缓存文件的操作需要更改后才能放在SAE上, 假设我们需要移植的代码如下:
<?php
file_put_contents(‘test.php’,'<?php echo “hello world”?>’);//写入缓存内容
include ‘test.php’;
?>
当然实际情况不可能这么简单,只写入一个hello world的文件。 实际情况可能是写入模板编译后的代码。SAE建议模版缓存使用Memcache实现。
如果使用Wrappers实现操作mecache很简单,只要在地址上加上前缀就可以了:
<?php
file_put_contents(‘saemc://test.php’,'<?php echo “hello world”?>’);//写入缓存内容
include ‘saemc://test.php’;
?>
如果使用原生的Memcache接口实现, 你可以这样写:
<?php
$m=memcache_init();
$m->set(‘test’,'<?php echo “hello world”?>’);//写入缓存内容
eval(‘?>’.$m->get(‘test’));//这句很关键
?>
以上代码先将缓存内容写入memcache中, 然后读取memcache缓存内容并用eval执行。
注:eval里面的”?>”别写漏掉了。 因为缓存内容带有php标签,你是不能用eval直接执行的。

上面只是说明了实现原理, 实际情况要比这复杂得多。
1,如何获得缓存的生成时间。
比如,我们的程序往往会判断模板缓存的生成时间,如果模板的修改时间大于缓存的生成时间就会重新编译缓存。
2,如何精确报错提示信息。
如果eval执行的代码有语法错误, 错误提示信息只会显示eval函数有错, 不能具体的显示到底是哪个缓存文件中的哪一行出错了。这会给我们开发带来不便。

下面这个类解决了以上的问题:
<?php
class SaeMC {

static public $handler;
static private $current_include_file = null;
static private $contents = array();
static private $filemtimes = array();

//设置文件内容
static public function set($filename, $content) {
self::$handler->set($_SERVER[‘HTTP_APPVERSION’] . ‘/’ . $filename, time() . $content, MEMCACHE_COMPRESSED, 0);
}

//载入文件
static public function include_file($_filename,$_vars=null) {
self::$current_include_file = ‘saemc://’ . $_SERVER[‘HTTP_APPVERSION’] . ‘/’ . $_filename;
$_content = isset(self::$contents[$_filename]) ? self::$contents[$_filename] : self::getValue($_filename, ‘content’);
if(!is_null($_vars))
extract($_vars, EXTR_OVERWRITE);

if (!$_content)
exit(‘<br /><b>SAE_Parse_error</b>: failed to open stream: No such file ‘ . self::$current_include_file);
if (@(eval(‘ ?>’ . $_content)) === false)
self::error();
self::$current_include_file = null;
unset(self::$contents[$_filename]); //释放内存
}

static private function getValue($filename, $type=’mtime’) {
$content = self::$handler->get($_SERVER[‘HTTP_APPVERSION’] . ‘/’ . $filename);
if (!$content)
return false;
$ret = array(
‘mtime’ => substr($content, 0, 10),
‘content’ => substr($content, 10)
);
self::$contents[$filename] = $ret[‘content’];
self::$filemtimes[$filename] = $ret[‘mtime’];
return $ret[$type];
}

//获得文件修改时间
static public function filemtime($filename) {
if (!isset(self::$filemtimes[$filename]))
return self::getValue($filename, ‘mtime’);
return self::$filemtimes[$filename];
}
//清空读取的缓存
static public function clearCache($filename){
if(isset(self::$contents[$filename])) unset(self::$contents[$filename]);
if(isset(self::$filemtimes[$filename])) unset(self::$filemtimes[$filename]);
}

//删除文件
static public function unlink($filename) {
if (isset(self::$contents[$filename]))
unset(self::$contents[$filename]);
if (isset(self::$filemtimes[$filename]))
unset(self::$filemtimes[$filename]);
return self::$handler->delete($_SERVER[‘HTTP_APPVERSION’] . ‘/’ . $filename);
}

static public function file_exists($filename) {
return self::filemtime($filename) === false ? false : true;
}

static function error() {
$error = error_get_last();
if (!is_null($error)) {
$file = strpos($error[‘file’], ‘eval()’) !== false ? self::$current_include_file : $error[‘file’];
exit(“<br /><b>SAE_error</b>:  {$error[‘message’]} in <b>” . $file . “</b> on line <b>{$error[‘line’]}</b><br />”);
}
}

}
if (!(SaeMC::$handler = @(memcache_init()))) {
header(“Content-Type:text/html; charset=utf-8”);
exit(‘您的Memcache还没有初始化,请登录SAE平台进行初始化~’);
}
register_shutdown_function(array(‘SaeMC’, ‘error’));
?>
使用方法:
<?php
include_once ‘SaeMC.class.php’;//加载类库
SaeMC::set(‘test.php’,'<?php echo “hello world”?>’);//设置缓存内容
SaeMC::include_file(‘test.php’);//执行缓存文件
var_dump(SaeMC::filemtime(‘test.php’));//获得缓存文件生成时间
var_dump(SaeMC::file_exists(‘test.php’));//判断文件是否存在
?>
说明:
1, SaeMC类生成的缓存名称都加了$_SERVER[‘HTTP_APPVERSION’]进行区分。 这个server变量是应用的版本号。 如果不加版本号进行区分可能会有缓存共享的问题。比如:同一个应用下有2个版本,这两个版本都是相同的程序, 会发现修改了版本1的模版, 看见版本2的内容也被修改了。
2,如果你需要传递变量, 可以启用include_file的第二个参数,它是一个数组,如:
<?php
$vars=array(‘a’=>’aaa’,’b’=>’bbb’);
SaeMC::include_file(‘test.php’,$vars);
?>
这时候,我们在test.php缓存中可以使用$a和$b变量。
另外说明一下为什么Wrappers的效率会比原生的Memcache低:
Wrappers的效率会比Memcache原生接口低, 可以通过代码测试
<?php
$start=  microtime(true);
file_put_contents(‘saemc://test’,’content’);
$end=  microtime(true);
var_dump($end-$start);//输出使用wrappers的运行时间
$start=  microtime(true);
$m=memcache_init();
$m->set(‘test’,’content’);
$end=  microtime(true);
var_dump($end-$start);//输出使用原生Memcache接口的运行时间
?>
通过输出的两个运行时间,会发现wrappers的效率比原生memcache的效率低了一两倍。
wrappers的效率低的其中一个原因是:wrappers每次操作memcache,实际操作了两次。我们用wrappers不仅生成了test缓存,还会生成一个test.meda的缓存,用于存储文件时间的meda信息。大家可以get一下这个test.meda缓存看看。
wrappers还有会重复多次读取同一个缓存的问题,如运行以下两行代码:
<?php
file_exists(‘saemc://test.php’);//判断缓存是否存在
filemtime(‘saemc://test.php’);//获得缓存生成时间
?>
上面两行代码虽然都是判断同一个缓存,但是wrappers会读取重复两次读同一个memcache缓存。

SaeMC类, 每次只会操作一次, 将缓存生成时间和缓存内容都放在同一个缓存中,不会同时操作两次。 而且避免了重复多次读取同一个缓存。所以性能会比wrappers高一些。
当然,有也有些特殊情况,有时候我们必须重复读取缓存。 如以下情况。
<?php
if(SaeMC::filemtime(‘cache_path’)<  filemtime(‘template_path’)){
//如果模版修改时间大于缓存生成时间,重新编译缓存
SaeMC::set(‘cache_path’,’some content’);
SaeMC::clearCache(‘cache_path’);//这里需要清空读取的缓存内容,这样一次才能读取最新编译缓存
}
SaeMC::include_file(‘cache_path’);
?>
上面代码中使用了 SaeMC::clearCache 清空了之前filemtime读取的缓存内容, 这样才能保证include_file的时候重复读取一次, 获得最新的编译缓存。

注:或许你除了测试运行时间外,还会通过测试wrappers和原生接口各自来内存使用情况来测试他们的性能。 这是不靠谱。内存使用高只能说明程序当时没有及时释放内存,并不能证明程序就真正占用了很高的内存。

Leave a Reply