一連の処理の中で複数の生存期間を使い分ける必要があり、こんな事をやっていたのだけどどうも上手く動かない。

$cache = new Cache_Lite(array('lifeTime' => null, 'automaticSerialization' => true));
if($data = $cache->get($cacheKeyData)):
  return $data;
else:
  $data = array();
  foreach($this->getFromDB() as $id):
    $post = $cache->get($cacheKeyPost)
    if(!$post):
      $post = $this->getPostById($id);
      $cache->setLifeTime(null);
      $cache->save($post, $cacheKeyPost);
    endif;
    $data[] = $post;
  endforeach;
  $cache->setLifeTime(60 * 60);
  $cache->save($data, $cacheKeyData);
endif;

やりたかったことを簡単に箇条書きにすると、

  • データ配列丸ごとキャッシュする。生存期間は短めに。
  • データ配列がキャッシュ切れの場合、個別に取得する。
  • 個別データは生存期間無期限でキャッシュする。

個別のデータは使い回されるし更新されないので、明示的にキャッシュを削除しない限りキャッシュデータを使うと言うことなのだけど、saveするときの生存期間の設定がキャッシュに反映されるのかと思ったらそうじゃないのね。そのキャッシュが有効かどうかは当然get時に判断されるのだけどソースではこんな感じ。

if ((file_exists($this->_file)) && (@filemtime($this->_file) > $this->_refreshTime)) {
  $data = $this->_read();
}

「$this->_refreshTime」というのは、setLifeTimeするときに現在時刻と比較して設定される「キャッシュが無効になる時刻」のこと。それとキャッシュファイルの最終更新時刻が比較されているだけなので、saveの時に「1分」と設定して保存しても、get時に「無制限」となってたらキャッシュは更新されない。あれま。save時に何らかの形で生存期間を記録しているのかと思ったけどそんなことはなかったぜ。

上の例ではデータ配列をgetするときの生存期間が「null」(無制限)なので、いつまで経ってもキャッシュが更新されないという状況になります。そうだったのか…まぁ、考えてみれば当たり前のことなんですけどねぇ。誤解してました。


例えばこうすればきちんと意図通りに動くんじゃないかな。

$cache = new Cache_Lite(array('lifeTime' => null, 'automaticSerialization' => true));
$cache->setLifeTime(60 * 60);
if($data = $cache->get($cacheKeyData)):
  return $data;
else:
  $data = array();
  foreach($this->getFromDB() as $id):
    $cache->setLifeTime(null);
    $post = $cache->get($cacheKeyPost)
    if(!$post):
      $post = $this->getPostById($id);
      $cache->save($post, $cacheKeyPost);
    endif;
    $data[] = $post;
  endforeach;
  $cache->save($data, $cacheKeyData);
endif;


テスト→無事、違う生存期間でキャッシュされたことを確認。
ていうか、キャッシュ取得時の生存期間設定に従うってどういうことだよ…



追記:「automaticCleaningFactor」の罠

オプションで「automaticCleaningFactor」に数値を設定すると、設定された数字の回数に従ってランダムにキャッシュがcleanされます(タイミングはキャッシュ保存時)。

cleanというのは、

  • キャッシュディレクトリの中にある全キャッシュをチェックして
  • 生存期間が終了したキャッシュを削除する

という仕組みで、要するに簡易のガベージコレクションですね。


しかーし、この仕組みも前述の生存期間に従って動作しているので、例えばcleanが発動したときの生存期間設定が5秒だった場合、5秒より以前に作成されたキャッシュは「すべて」削除されてしまいます。まじか!いやマジです。妙にキャッシュが削除されるなぁと思ったらこれでした。

というわけで複数の生存期間が存在するように使う場合には、

  • 「automaticCleaningFactor」を使わない
  • 生存期間ごとにキャッシュディレクトリを分ける
  • そもそもCache_Liteを使わない

という感じで対応せざるを得ないですね。


やっぱり「Lite」は「Lite」だよなぁ……

簡易にキャッシュする以上のことをしたいのなら素直にmemcached入れるかKVS(今流行りのって何だろう?)を使った方が良いかな…。一応、PEAR::Cacheという案もなくはないけども。Kyoto Tycoonとかどうなんだろう。