ZF + memcachedを使用する場合のキャッシュとタグ

まえがき




Zend Framework + Memcachedバンドルを使用した開発プロセス中に、既存のフレームワーク機能の(過剰な)豊富さと特定の制限の両方に遭遇することがあります。 これらのケースの1つと、この記事で見つかった解決策についてお話します。



問題の説明


ご存じのように、Memcachedは、シンプルで必要かつ十分な機能を備えた比較的簡単に使用できるキー/値ストレージです。 Memcachedとやり取りするためにZFが提供するインターフェイスは、キャッシュを操作するための一般的なライブラリに含まれています(Sqlite、Xcache、ZendServerなどのアダプタも含まれます)。 これらのキャッシングシステムの一部は、キャッシュされたオブジェクトのタグの使用をサポートしますが、Memcachedにはそのような機能がないため、Memcachedを操作するときにタグ付きのオブジェクトをキャッシュするために標準のZFクラスインターフェイスを使用しようとすると、例外まで(ログに)エラーが発生します。 ( ドキュメントで詳細を読むことができます )。







開発におけるタスクの1つは、次の意味でのキャッシュの「スマートな」使用でした。





したがって、クエリの結果をキャッシュ内のデータベースに保存するときに、タグを追加する必要があります。タグを使用して、キャッシュに格納されている値が属するオブジェクト/リスト/モデルを判別できます。 はい、このタグを保存することも何らかの形で必要です。



かつて、バンドルmemcached + MongoDBを使用したキャッシュからのスマートな削除に関するHaberの記事は、問題を解決するためのいくつかのアイデアを促しました。 ただし、MongoDBをサーバーに追加することはできませんでした。 現在および近い将来、他のキャッシングシステム(Redisなど)はサーバー上で提供されません。



ソリューションとコード


基本的な考え方は単純です。特定のタグのすべてのキャッシュキーを配列に保存します。キャッシュ自体に、この配列のキャッシュキーとしてタグを使用します。 この場合、タグ自体は、要求のタイプとキャッシュキー自体に基づいて自動モードで構築されます。

ツリーにさらに考えを広めることなく、プロジェクトで使用されるすべてのマッパーが継承されるベースマッパーコードの最初の部分をすぐに調べます(問題自体を解決するためのコードに加えて、この例には、システムが動作するために必要であり、タスクに部分的に関連する一定量の付随コードがあります) 。 大量のコードがあると思われる場合はすぐに謝罪しますが、十分なコメントを残しました。



class System_ModelMapper { protected $_dbTable; //         protected $_modelName = ''; //  ,       -      public function __construct() { parent::__construct(); if (empty($this->_modelName)) { $parts = explode('_', get_class($this)); $this->_modelName = str_replace('Mapper', '', $parts[2]); } } //      dbtable    /** * @return /Zend_Db_Table_Abstract */ public function getDbTable() { if (null === $this->_dbTable) { $this->setDbTable('Application_Model_DbTable_' . $this->_modelName); } return $this->_dbTable; } public function setDbTable($dbTable) { if (is_string($dbTable)) { $dbTable = new $dbTable(); } if (!$dbTable instanceof Zend_Db_Table_Abstract) { throw new Exception('Invalid table data gateway provided'); } $this->_dbTable = $dbTable; return $this; } /** *       * @param array $params * @return System_Model */ public function getModel($params = array()) { $getInstance = 'Application_Model_' . $this->_modelName; return new $getInstance($params); } … }
      
      







次に、単一のオブジェクトを見つけるためのいくつかの方法があります。



  /** *             * @param array $data   * @return string */ protected function objectCacheId($data) { $fields = array_keys($data); $values = md5(json_encode(array_values($data))); return 'find_' . $this->_modelName . '_' . join('_', $fields) . '_' . $values; } /** *           * @param $object System_Model * @return string */ public function getObjectCacheTag($object) { return 'object_' . $this->_modelName . '_' .$object->get_id(); } /** *       * @param numeric $id  ID * @param mixed $obj ,      * @param bool $cache         * @return bool|System_Model */ public function find($id, $obj = false, $cache = false) { return $this->findByFields(array('id' => $id), $obj, $cache); } /** *      * @param array $data     * @param mixed $obj ,      * @param bool $cache         * @return bool|System_Model */ public function findByFields($data, $obj = false, $cache = false) { //     -    ,        (        ,         (   :) ) if ($cache) { $cacheId = $this->objectCacheId($data); if (Zend_Registry::isRegistered(CACHE_NAME) { /** @var $cache System_Cache_Core */ $cache =& Zend_Registry::get(CACHE_NAME); //      -    if ($cache->test($cacheId)) { return $cache->load($cacheId); } } else { $cache = false; } } //   Zend_Db_Table         $select = $this->getDbTable()->select(); foreach ($data as $field => $value) { $select->where($select->getAdapter()->quoteIdentifier($field) . ' = ?', $value); } $row = $this->getDbTable()->fetchRow($select); if ($row) { if ($obj === false) { $obj = $this->getModel(); } $obj->setOptions($row->toArray()); } else { $obj = false; } //    -     if ($cache) { $cache->save($obj, $cacheId); } return $obj; }
      
      







指定されたコードによると-ご覧のとおり、オブジェクト検索メソッドにはタグシステムに関する手がかりがありません。 $ cacheオブジェクトレベルで動作します。

次に、ページネーションとソートを考慮して、テーブル内のオブジェクトのセットまたはすべてのオブジェクトを見つけるためのいくつかの方法があります。



  /** *          ,    * @param array $data   * @param bool|string|array $order   * @param bool|System_Paginator $paginator     * @return string */ protected function listCacheId($data = array(), $order = false, $paginator = false) { $fields = array_keys($data); $values = md5(json_encode(array_values($data))); return sprintf('%s_%s_%s_%s_%s', $this->getListCacheTag(), join('_', $fields), $values, empty($order) ? '' : md5(json_encode($order)), is_object($paginator) ? $paginator->page . '_' . $paginator->limit : '' ); } /** *         * @return string */ public function getListCacheTag() { return 'list_' . $this->_modelName; } /** *        * @param array $data   * @param bool|string|array $order   * @param bool|System_Paginator $paginator     * @param bool|string $cache         */ public function fetchByFields($data = array(), $order = false, $paginator = false, $cache = false) { if ($cache) { $cacheId = $this->listCacheId($data, $order, $paginator); $cache .= 'Cache'; if (Zend_Registry::isRegistered(CACHE_NAME)) { /** @var $cache System_Cache_Core */ $cache =& Zend_Registry::get(CACHE_NAME); if ($cache->test($cacheId)) { return $cache->load($cacheId); } } else { $cache = false; } } //   ,           ,    $select = $this->getDbTable()->select(); $select_paginator = $this->getDbTable()->select(true); foreach ($data as $field => $value) { $s = '='; // value      ('=', 2)  ('<=', 10) if (is_array($value)) { $s = $value[0]; $value = $value[1]; } $select->where($select->getAdapter()->quoteIdentifier($field) . " $s ?", $value); $select_paginator->where($select->getAdapter()->quoteIdentifier($field) . " $s ?", $value); } //    if (!empty($order)) { $select->order($order); } else { $select->order('id ASC'); } //   ,     if (is_object($paginator)) { //        ,    $fetch_count = $this->getDbTable()->fetchRow($select_paginator->columns('count(id) as _c'))->toArray(); $paginator->total = $fetch_count['_c']; //    ,     ,     if ($paginator->page > $paginator->getLastPage()) $paginator->page = $paginator->getLastPage(); //       $select->limitPage($paginator->page, $paginator->limit); } $resultSet = $this->getDbTable()->fetchAll($select); $result = $this->rowsToObj($resultSet); //      -         (        limit) if (is_object($paginator)) { $paginator->inlist = count($result); } if ($cache) { $cache->save($result, $cacheId); } return $result; } /** *         * @param bool|string|array $order   * @param bool|System_Paginator $paginator     * @param bool|string $cache         * @return array|bool */ public function fetchAll($order = false, $paginator = false, $cache = false) { return $this->fetchByFields(array(), $order, $paginator, $cache); } /** *         * @param Zend_Db_Table_Rowset_Abstract $rowset     * @return array|bool */ protected function rowsToObj($rowset) { if (!empty($rowset)) { $entries = array(); foreach ($rowset as $row) { /** @var $entry System_Model */ $entry = $this->getModel($row->toArray()); $entries[$entry->get_id()] = $entry; } return $entries; } return false; }
      
      







与えられたコードによると、サンプリング方法自体はタグシステムに直接関連していないこともここで見られます。



次に、標準のZend_Cache_Coreを継承し、Memcachedを操作するためにブートストラップオブジェクトを初期化するときに使用されるクラスコードを提供します。 その後、再びマッパーに戻り、データベース内のオブジェクトを保存、更新、削除するためのメソッドに戻ります。



 class System_Cache_Core extends Zend_Cache_Core { /** *    save   */ public function save($data, $id = null, $tags = array(), $specificLifetime = false, $priority = 8) { //      $ida = explode('_', $id); //        ,          switch ($ida[0]) { case 'list': //       2   (    ) $tag = join('_', array_splice($ida, 0, 2)); $this->updateTagList($tag, $id); break; case 'find': //   -       if ($data instanceof System_Model) { $tag = $data->get_mapper()->getObjectCacheTag($data); $this->updateTagList($tag, $id); } break; } //           return parent::save($data, $id, $tags, $specificLifetime, $priority); } /** *        * @param string $tag * @param string $cacheId */ public function updateTagList($tag, $cacheId) { //       $list = $this->getListByTag($tag); $list[] = $cacheId; //         $this->saveListByTag($tag, $list); } /** *      * @param string $tag */ protected function getListByTag($tag) { $tagcacheId = '_taglist_' . $tag; $list = array(); if ($this->test($tagcacheId)) { $list = $this->load($tagcacheId); } return $list; } /** *         * @param string $tag * @param array $list */ protected function saveListByTag($tag, $list) { $tagcacheId = '_taglist_' . $tag; $this->save($list, $tagcacheId); } /** *         * @param System_Model $object */ public function removeByObject($object = null) { if ($object instanceof System_Model) { //       $this->removeByTag($object->get_mapper()->getListCacheTag()); //             if ($object->get_id()) { $this->removeByTag($object->get_mapper()->getObjectCacheTag($object)); } } } /** *         * @param string $tag */ public function removeByTag($tag) { //      $list = $this->getListByTag($tag); //      foreach ((array)$list as $cacheId) { $this->remove($cacheId); } //      , ,    $this->saveListByTag($tag, array()); } }
      
      







さて、オブジェクトを保存および削除するためのマッパーメソッドについては言及する必要があります。



  /** *     * @param System_Model $object   * @param boolean $isInsert    * @return array|bool|mixed */ public function save($object, $isInsert = false) { $data = $object->toArray(); $find = array('id = ?' => $object->get_id()); if (null === ($id_value = $object->get_id())) { $isInsert = true; unset($data['id']); } if ($isInsert) { $pk = $this->getDbTable()->insert($data); if ($pk) { $object->set_id($pk); } $this->resetCache(); return $pk; } else { //    -       return $this->getDbTable()->update($data, $find) && $this->resetCache($object); } } /** *     * @param $object System_Model * @return array|bool|mixed */ public function insert($object) { return $this->save($object, true); } /** *     * @param $object System_Model   * @return bool */ public function remove($object) { $primary = $this->getDbTable()->get_primary(); $where = array('id = ?' => $object->get_id()); //   -      return ($this->getDbTable()->delete($where) && $this->resetCache($object)); } /** *         * @param System_Model $object * @param array $cacheIds * @return bool */ public function resetCache($object = null, $cacheIds = array()) { //       if (Zend_Registry::isRegistered(CACHE_NAME)) { /** @var $cache System_Cache_Core */ $cache = Zend_Registry::get(CACHE_NAME); if (!empty($object)) { //       $cache->removeByObject($object); } else { //     $cache->removeByTag($this->getListCacheTag()); } foreach ($cacheIds as $cacheId) { $cache->remove($cacheId); } } return true; } }
      
      







その結果、オブジェクトとモデル用の特定の基本的なタグシステムが得られました。これは使用時に透過的であり、モデルまたはマッパー自体のいずれかでタグ名を明示的に示す必要はありません-タグ名はモデル自体とオブジェクトのデータに基づいて自動的に生成されます。



ちなみに、問題とささいなこと(実行および解決が計画されています):





これらの問題はすべて、何らかの方法で解決されます。

しかし、私は経験豊富な住民のレビューを聞いて、提案された方法に対する批判をかなり受けたいです。



PS近日公開予定!



All Articles