Enhance performance with Zend_Cache

January 7, 2010

Zend Framework

So I finally took the Zend Framework plunge a few weeks ago. No, I didn’t start building an application with the framework, but I did start investigating some of the ZF classes for stand-alone implementation in my existing projects.

The first ZF class to catch my eye was Zend_Cache, for its obvious performance implications. The applications I develop and manage are very heavy with database transactions. Hitting the db every time I need an object is a serious performance bottleneck, and on a shared environment can be troubling to other applications living in the same environment.

I’m going to describe my experiences with Zend_Cache here, but will not bore you with lots of code detail and specifics. The above-linked reference page on the ZF site has more than adequate documentation.

Zend_Cache is incredibly flexible, and allows you to use any one of a number of back end caching methods…i.e. the place you want to store your cached data. Each of these methods is implemented with an adapter class. Your choices include file-based caching, sqlite, APC, memcached, Xcache, and ZendPlatform. There are additional adapters to permit caching in two different methods (TwoLevels) and for methods specific to Zend Server.

The enterprise-shared hosting environment my apps live on includes APC, so I went with that adapter. My dev environment runs Zend Server CE, which includes an APC-compatible Zend caching extension, so I enabled it through the Zend Server config pane.

There are also a few front end adapters to use, which operate the caching machinery. Zend_Cache_Core is the base adapter and is, in Zend’s words, generic and flexible. However, for more exotic solutions are available: File, Function, Output, etc. Refer to the ZF documentation page for explanations of each of these.

You can cache just about any kind of data that you can store in a variable: integers, strings, arrays, serialized stuff, and even objects. Think about the possible implementations and their performance benefits for just a moment: Object persistence across sessions, recordset caching for speedier searches and paged results… I suppose one could even use this for an alternative session manager.

I chose to implement Zend_Cache to store objects built from (sometimes multiple) database queries. I can now access and manipulate objects across multiple page requests and reduce database hits to only when the object is being modified and saved.

The implementation is extremely simple. In the following code example I will demonstrate how to create a cached object. Let’s assume we already have an instance of the object we want to cache ($this). I am not providing a complete class below (so don’t try to cut and paste the code literally).

// we need a method create a unique ID for the object
// this will be used to identify the cached object when created or when retrieving
// I would suggest using a hash that includes your db unique id
public function create_cache_id($id) {
    $cache_id = md5('myRecord_' . $id);
    return $cache_id;
}

// this method will retrieve the object
// $id should be unique id of your database record
public function fetch($id) {

    $cache_id = $this->create_cache_id($id);

    $frontendOptions = array(
        'lifetime' => 300 ,                 // cached object will expire after this many seconds
        /* this ROCKS because you don't spend time writing code to check if the cache is expired */
        'automatic_serialization' => true   // we'll need this on to store the object, also for an array
    );

    // note that for the APC adapter there are no back end options to configure
    // instantiate instance of Zend_Cache

    $cache = Zend_Cache::factory('Core', 'APC', $frontendOptions);

    if(!($obj = $cache->load($this->cache_id)) ) {

        /* if a cache with this ID doesn't exist, then execute your db query and build your object, then we'll add it to cache: */

        $this->myQuery($id); // this is just a generic method meant to represent your db transation

        $cache->save($this); // adds the object to cache

        echo 'this is not from cache'; // diagnostic, comment out later

    } else {

        /* if the cached object does exist, then we need to re-populate our object properties */

	// loop through each element of $obj and add as a property to $this
        foreach($obj as $key => $val) {
            $this->$key = $val;
        }

        echo 'this is from cache';  // diagnostic, comment out later

    }

}

I’ve grossly oversimplified the external parts of the process such as the parent object class and the db interactions, but this should still give you the idea. Any time we generate an object, we’ll automatically check the cache first before querying the database.

We also need a method to clear the cache if the object properties are modified. For example, if a user edits a record using an input form, we need to update the db and the cache. Rather than immediately re-loading the object into cache, I prefer to simply clear it out of cache and wait for the next fetch request to add back into cache. This way you don’t have a bunch of objects stored in cache that aren’t being used. Therefore, the methodology is to update the db and clear the cache.

The approach to clearing the cache is extremely simple. Let’s assume we already have a method for processing an update to a database record that underlies our object. To clear the old version of the object from cache, we simply call a method like this:

public function clear_cached($id) {

       $cache_id = $this->create_cache_id($id);
       $cache = Zend_Cache::factory('Core', 'APC');
       $cache->remove($cache_id);

}

It is truly not much more complicated than the above examples, particularly if you already have an existing application structure you want to add Zend_Cache to. I’ve been pleased with my first experiments into Zend Framework and intend to explore some other components for use in my projects. Hopefully this tutorial will help someone else out there who’s been hesitant to buy into the framework craze. I’m not “there” yet in terms of building my first framework-based app, but as I become more familiar with the ZF bits and part, I expect that to change.

Further Reading: Here are a couple of other great resources on Zend_Cache:

Note: This post also appears on my personal blog at ChrisRenner.com

About rennercr

Full time PHP/MySQL Developer for a large university/research hospital in the Southeast USA.

View all posts by rennercr

5 Responses to “Enhance performance with Zend_Cache”

  1. derrida Says:

    hi

    i do feel that zend_cache is great. one problem i face in this moment and cannot see t he way out is : how to use the paginator with a cached version.

    i have a query in the model and i pass it to the paginator and i return the paginator to the controller. i cannot figure out how to cache the query (this bit i can do) and then send the cache to the paginator.

    hope you can help me:)

    best regards

  2. rennercr Says:

    This is actually pretty simple. Just pass the entire SQL statement through the md5 function as the cache id. The SQL should contain the limit clause that will make that "page" unique in terms of cache id.

  3. derrida Says:

    thanks for the answer.

    what i have in this moment (before reading your article) is this (sorry for the long code, but i hope that if you`ll see the whole thing you will be able to help me):

    <?php
    class Bloging_Model_Post extends Zend_Db_Table_Abstract
    {
    protected $_name = ‘posts’;
    protected $_primary = ‘id’;

    public function getPostsByCategory($category,$page = 1, $order = "date",$paginationCount)
    {
    //the query
    $query = $this->select()->from(array(‘posts’),array(
    ‘posts.id’,'posts.title’,'posts.date’,'posts.short_desc’,'posts.text’,'posts.category_id’,'posts.status’,'posts.author’
    ));
    $query->where("category_id = ? ", $category);
    $query->where(‘status = ?’, 1);
    $query->order(‘date DESC’);

    //configure cache
    $frontend = array(
    ‘lifetime’ => 86400,
    ‘automatic_cleaning_factor’ => 100,
    ‘automatic_serialization’ => true
    );

    $backend = array(
    ‘cache_dir’ => ‘../cache’
    );

    $cache = Zend_Cache::factory(
    ‘core’,
    ‘file’,
    $frontend,
    $backend
    );

    // Tell paginator to use the cache
    $paginator = Zend_Paginator::setCache($cache);

    // Create a paginator based on query
    $paginator = new Zend_Paginator(new Zend_Paginator_Adapter_DbTableSelect($query));
    $paginator->setItemCountPerPage($paginationCount);
    $paginator->setCurrentPageNumber($page);
    return $paginator;

    }
    }

    as you can see. i create the query then the cach.then i set the cache with the paginator and return the paginator. but there are 2 problems:

    1- every time i refresh the page the cached file`s time is updated, therefore (if i understand) i do not use the cached version.

    2- if i add an item in the admin section, a new cache file is being generated.

    best regards

  4. rennercr Says:

    You’re not checking to see if a cache already exists first before saving to Zend_Cache.

    You need something like this:

    if(!($obj = $cache->load($this->cache_id)) ) {
    // if cache isn’t found
    $this->myQuery($id); // execute query
    $cache->save($this); // save to cache

    }

    This will test the cache and return its contents to $obj if a cache exists. If not, your query will run and also be saved to cache.

  5. derrida Says:

    hi

    i added an id and an if stetment. i entered a new item from mt admin and the cached version has been updated to include the new item. but that should not be the case right?

    so i`m not sure what am i doing wrong. i read your article and your answers (which sounds right) but i must be missing something.

    here is my improved code after trying to adapt to your answers:
    public function getPostsByCategory($category,$page = 1, $order = "date",$paginationCount)
    {

    //configure cache
    $frontend = array(
    ‘lifetime’ => 86400,
    ‘automatic_cleaning_factor’ => 100,
    ‘automatic_serialization’ => true
    );

    $backend = array(
    ‘cache_dir’ => ‘../cache’
    );

    $cache = Zend_Cache::factory(
    ‘core’,
    ‘file’,
    $frontend,
    $backend
    );

    //cacheID
    $cachePostId = ‘posts_’ . $category;

    //the query
    $query = $this->select()->from(array(‘posts’),array(
    ‘posts.id’,'posts.title’,'posts.date’,'posts.short_desc’,'posts.text’,'posts.category_id’,'posts.status’,'posts.author’
    ));
    $query->where("category_id = ? ", $category);
    $query->where(‘status = ?’, 1);
    $query->order(‘date DESC’);

    if (!($obj = $cache->load($cachePostId)))
    {

    $result = $this->fetchAll($query);
    $cache->save($result,$cachePostId);
    }

    // Create a paginator based on query
    $paginator = new Zend_Paginator(new Zend_Paginator_Adapter_DbTableSelect($query));
    $paginator->setItemCountPerPage($paginationCount);
    $paginator->setCurrentPageNumber($page);

    return $paginator;

    }

    best regrads

    derrida