memcachedを用いた関数キャッシュ

昨年末のPHP東京勉強会でちょっと話したネタ。

これまでは関数キャッシュにはPEAR::Cache_Lite_Functionとかを使っていたのだけど、キャッシュ内容をディスクに書かれるのは遅い気がするし、最近はやぱしmemcachedでしょということで、Memcache extensionを使った関数キャッシュクラスを作って使っている。

実際使っているクラスはいろいろ他のファイルに依存してたりするので、その依存性を取り除いたものがこのMemcache_Function.php

<?php
if (!function_exists('array_val')) {
    function array_val(&$data, $key, $default = null) {
        if (!is_array($data)) {
            return $default;
        }
        return isset($data[$key])? $data[$key]: $default;
    }
}

class Memcache_Function {
    /* defaults */
    static public $host = 'localhost';
    static public $port = 11211;

    protected $memcache = null;
    public $options = array();

    protected function __construct() {
        $host = self::$host;
        $port = self::$port;

        if (class_exists('Memcache', false)) {
            $mc = new Memcache;
            if (is_string($host)) {
                $result = $mc->connect($host, $port);
                if (!$result) {
                    throw new Exception('Can not connect to memcached: '. $host);
                }
            } else if (is_array($host)) {
                foreach ($host as $h) {
                    $result = $mc->addServer($h, $port);
                    if (!$result) {
                        throw new Exception('Can not connect to memcached: '. $h);
                    }
                }
            } else {
                throw new Exception('Invalid host configuration of memcached');
            }
            $this->memcache = $mc;
        } else {
            // Log_warning('Memcache disabled');
        }
    }

    protected function callFunc() {
        $args = func_get_args();
        $key = '';

        // args:0 is lifeTime
        $lifetime = array_shift($args);

        // args:1 is function
        $func = array_shift($args);

        // args:2- are args for original function

        if (!empty($this->memcache)) {
            $callee = array(
                'func' => $func,
                'args' => $args,
                );
            $serialized = serialize($callee);
            $key = md5($serialized);

            $var = $this->memcache->get($key);
            if (($var !== FALSE) &&
                ($var['serialized'] === $serialized)) {
                return $var['result'];
            }
        }

        // call real function
        $result = call_user_func_array($func, $args);

        if (!empty($this->memcache)) {
            $flag = (array_val($this->options, 'compress', false))?
                MEMCACHE_COMPRESSED: null;
            $var = array(
                'serialized' => $serialized,
                'result' => $result,
                );
            $this->memcache->set($key, $var, $flag, $lifetime);
        }
        return $result;
    }

    static public function call() {
        $_this = self::getInstance();
        // variable arguments
        $args = func_get_args();
        return call_user_func_array(array($_this, 'callFunc'), $args);
    }

    static public function setOption($options) {
        $_this = self::getInstance();
        $_this->options = array_merge($_this->options, $options);
    }

    protected function _flush() {
        if (!empty($this->memcache)) {
            $this->memcache->flush();
        }
    }

    static public function flush() {
        $_this = self::getInstance();
        $_this->_flush();
    }

    // Singleton Interface

    private static $_singleton = null;

    static public function getInstance() {
        if (self::$_singleton == null) {
            self::$_singleton = new self;
        }
        return self::$_singleton;
    }
}

?>

使い方は、キャッシュしたい関数をMemcache_Function::call()を通して呼び出す。

<?php
require_once 'Memcache_Function.php';

function hoge($a) {
    return $a . $a;
}

$result = Memcache_Function::call(30, 'hoge', 'A');
var_dump($result);
?>

2回目の呼び出しから、実際に関数は呼び出さずにキャッシュされた内容が返る。

Memcache_Function::call()の引数は以下の通り

  • キャッシュ有効期間(秒数)
  • 呼び出し関数名。call_user_func()で呼び出せる形式ならば何でもOK。
  • 関数への引数(任意個数)。

あと、キャッシュするのは関数の返値のみです。Cache_Lite_Functionのように出力結果もキャッシュしたい場合は、callFuncの中で適当にob_ほげほげするように書き換えれば良いと思います。まぁ難しくないよね。

おまけ

勉強会の場でPythonianの露木さんから、こういうdelegationみたいなクラスはPythonならば一行で書けるといわれました。やっぱり考えられてる言語は違うなあ。まぁこういう無駄にがんばるマゾ的な喜びもPHP者の宿命ということで。