Greg Aker

CodeIgniter Reactor's Caching Drivers

Filed in: PHP, CodeIgniter

February 12, 2011

Before Reactor dropped, I snuck in some caching drivers (Docs Link) that Pascal had been tossing back and forth for several months. I've seen a few people talk about them, but I think they are still generally unknown so I want to give a brief intro on how to use them.

At this time, there are drivers available for APC, Memcached, file-based caching, and a dummy cache. If you haven't already, head over to BitBucket and get cloning or forking.

Examples

Caching API Calls

We can cache API calls pretty easily within controllers. Let's say we want to grab 5 most recent tweets from our Twitter Timeline and display it on the front end of our site. OAuth is a bit overkill here, so we do need to deal with the rate limiting Twitter enforces. In order to keep the code sparse, I'm intentionally being really lax on error checking, etc. This isn't an exercise in how to properly get tweets from Twitter. :)

<?php

class Tweet extends CI_Controller {

    public function index()
    {
        $this->output->enable_profiler();

        // Load up drivers
        $this->load->library('driver');
        $this->load->driver('cache', array('adapter' => 'apc'));

        // Get Tweets from Cache
        $tweets = $this->cache->get('my_tweets');

        if ( ! $tweets)
        {
            // No tweets in the cache, so get new ones.
            $url = 'http://api.twitter.com/1/statuses/user_timeline.json?screen_name=gaker&count=5';

            $tweets = json_decode(file_get_contents($url));
            $this->cache->save('my_tweets',$tweets, 300);
        }

        $data = array(
            'title'     => 'tweet tweet',
            'tweets'    => $tweets
        );

        $this->load->view('tweets', $data);
    }
}

Then a really super simple view could be:

<?php foreach ($tweets as $twat): ?>
    <?=$twat->text?>
    <hr>
<?php endforeach; ?>

Then let's see what Seige has to say:

> siege -c 3 -b -t 10s http://localhost/personal/codeigniter-reactor/index.php?/tweet
** SIEGE 2.69
** Preparing 3 concurrent users for battle.
The server is now under siege...

Lifting the server siege...      done.
Transactions:               3216 hits
Availability:             100.00 %
Elapsed time:               9.73 secs
Data transferred:           5.96 MB
Response time:              0.01 secs
Transaction rate:         330.52 trans/sec
Throughput:             0.61 MB/sec
Concurrency:                2.97
Successful transactions:        3216
Failed transactions:               0
Longest transaction:            0.03
Shortest transaction:           0.00

So if you're running a single web server/VPS APC can be fantastic to cache API requests in.

Stupid Database Queries

For this example, I'm going to just load up my development ExpressionEngine database in this CI app, and try to come up with some bad queries. While we can still use APC in this instance, I'm going to illustrate how to do this with Memcached.

In my local development ExpressionEngine database, I have about 1600 members, and round about 4,000 entries. I'm going to first select all the member ids, cycle through them doing a query to get the title and a bit more info on the entry. So this is going to be just entirely stupid and inefficient. Here we go:

<?php

class Queries extends CI_Controller
{
    public function index()
    {
        $this->output->enable_profiler();
        $this->load->database();

        $members = $this->db->select('member_id')->get('exp_members');

        $res = array();

        foreach ($members->result() as $m)
        {
            $qry = $this->db->select('ct.entry_id, ct.title, ct.url_title,
                                      ct.entry_date, m.member_id, m.screen_name, 
                                      m.group_id')
                            ->where('ct.author_id', (int) $m->member_id)
                            ->where('ct.status', 'open')
                            ->join('exp_members m', 'm.member_id = ct.author_id')
                            ->order_by('ct.entry_date', 'ASC')
                            ->get('exp_channel_titles ct');

            $res[] = $qry->result();
        }
    }
}

Based on my dataset, this lunacy produces 1605 queries, and is horribly inefficient. We can get around it some with memcached. That's not to say that just because you can toss caching at it you should do it, but I just want to illustrate how it can help :) So, with this controller, seige gives us:

> siege -c 3 -b -t 10s http://localhost/personal/codeigniter-reactor/index.php?/queries
** SIEGE 2.69
** Preparing 3 concurrent users for battle.
The server is now under siege...

Lifting the server siege...      done.
Transactions:                  3 hits
Availability:             100.00 %
Elapsed time:               9.02 secs
Data transferred:           0.33 MB
Response time:              5.17 secs
Transaction rate:           0.33 trans/sec
Throughput:             0.04 MB/sec
Concurrency:                1.72
Successful transactions:           3
Failed transactions:               0
Longest transaction:            5.27
Shortest transaction:           4.99

And it's pretty evident that MySQL is getting the living shit beat out of it. This is no good for your visitors. If we cache this mess with memcached, the controller turns into:

<?php

class Queries extends CI_Controller
{
    public function index()
    {
        $this->output->enable_profiler();
        $this->load->database();

        // Load Drivers
        $this->load->library('driver');
        $this->load->driver('cache', array('adapter' => 'memcached'));

        $res = $this->cache->get('my_mess', 300);

        if ( ! $res)
        {
            $members = $this->db->select('member_id')->get('exp_members');

            $res = array();

            foreach ($members->result() as $m)
            {
                $qry = $this->db->select('ct.entry_id, ct.title, ct.url_title,
                                          ct.entry_date, m.member_id, 
                                          m.screen_name, m.group_id')
                                ->where('ct.author_id', (int) $m->member_id)
                                ->where('ct.status', 'open')
                                ->join('exp_members m', 'm.member_id = ct.author_id')
                                ->order_by('ct.entry_date', 'ASC')
                                ->get('exp_channel_titles ct');

                $res[] = $qry->result();
            }

            $res = json_encode($res);
            $this->cache->save('my_mess', $res, 300);
        }

        var_dump(json_decode($res));
    }
}

Results from seige are:

> siege -c 3 -b -t 10s http://localhost/personal/codeigniter-reactor/index.php?/queries
** SIEGE 2.69
** Preparing 3 concurrent users for battle.
The server is now under siege...

Lifting the server siege...      done.
Transactions:                432 hits
Availability:             100.00 %
Elapsed time:               9.53 secs
Data transferred:           2.91 MB
Response time:              0.07 secs
Transaction rate:          45.33 trans/sec
Throughput:             0.31 MB/sec
Concurrency:                2.98
Successful transactions:         432
Failed transactions:               0
Longest transaction:            0.10
Shortest transaction:           0.05

So we've increased our output from 3 hits to 432 in 10 seconds. Not a bad boost, if I do say so myself. In a 'real' application, my query calls would be in the model, but we can also avoid loading the models unless the cache is stale, which saves on an include(), and helps to keep CI breezing along even faster!

Have you checked out the new cached drivers in CI Reactor? If so, how do you find them stacking up with your applications needs?