Memcache & MySQL PHP Session Handler

Posted on April 8, 2009. Filed under: Memcache, PHP, mysql+ | Tags: , , , , |

LAST UPDATED MAY 17, 2009

I have recently read Cal Henderson’s book, Building Scalable Web Sites, and was inspired 6 ways from Sunday on just about every approach I take to web development. I highly recommend you purchase this book, and read through it asap… it is a must read [even though it was published back in 2006]. My friend Erik Kastner recommended it to me, and now I’m recommending it to you :-)

Anyways, back on topic… One of the major concepts I picked up was that of Write Through Caches. Write Through Caches are explained in depth all throughout the internetS, so I leave you with this: tinyurl.com/cgeobs. This script also solves the scalability issue you will experience when you move your web site / application to more than one web server. If you put a Memcache and MySQL daemon on one box and have all your web servers connect to it, your sessions are in one centralized place [though, this doesn't account for fail over].

I also built in a simple check to see if the session data had changed before writing it to the DB. Most of the time it doesn’t, so this should offer some more performance.

This was written for PHP5, if you’re still using PHP4, click here.

This script assumes you have already connected to your database.


<?php
    class SessionHandler {
        public $lifeTime;
        public $memcache;
        public $initSessionData;

        function __construct() {
            # Thanks, inf3rno
            register_shutdown_function("session_write_close");

            $this->memcache = new Memcache;
            $this->lifeTime = intval(ini_get("session.gc_maxlifetime"));
            $this->initSessionData = null;
            $this->memcache->connect("127.0.0.1",11211);

            return true;
        }

        function open($savePath,$sessionName) {
            $sessionID = session_id();
            if ($sessionID !== "") {
                $this->initSessionData = $this->read($sessionID);
            }

            return true;
        }

        function close() {
            $this->lifeTime = null;
            $this->memcache = null;
            $this->initSessionData = null;

            return true;
        }

        function read($sessionID) {
            $data = $this->memcache->get($sessionID);
            if ($data === false) {
                # Couldn't find it in MC, ask the DB for it

                $sessionIDEscaped = mysql_real_escape_string($sessionID);
                $r = mysql_query("SELECT `sessionData` FROM `tblsessions` WHERE `sessionID`='$sessionIDEscaped'");
                if (is_resource($r) && (mysql_num_rows($r) !== 0)) {
                    $data = mysql_result($r,0,"sessionData");
                }

                # Refresh MC key: [Thanks Cal :-)]
                $this->memcache->set($sessionID,$data,false,$this->lifeTime);
            }

            # The default miss for MC is (bool) false, so return it
            return $data;
        }

        function write($sessionID,$data) {
            # This is called upon script termination or when session_write_close() is called, which ever is first.
            $result = $this->memcache->set($sessionID,$data,false,$this->lifeTime);

            if ($this->initSessionData !== $data) {
                $sessionID = mysql_real_escape_string($sessionID);
                $sessionExpirationTS = ($this->lifeTime + time());
                $sessionData = mysql_real_escape_string($data);

                $r = mysql_query("REPLACE INTO `tblsessions` (`sessionID`,`sessionExpirationTS`,`sessionData`) VALUES('$sessionID',$sessionExpirationTS,'$sessionData')");
                $result = is_resource($r);
            }

            return $result;
        }

        function destroy($sessionID) {
            # Called when a user logs out...
            $this->memcache->delete($sessionID);
            $sessionID = mysql_real_escape_string($sessionID);
            mysql_query("DELETE FROM `tblsessions` WHERE `sessionID`='$sessionID'");

            return true;
        }

        function gc($maxlifetime) {
            # We need this atomic so it can clear MC keys as well...
            $r = mysql_query("SELECT `sessionID` FROM `tblsessions` WHERE `sessionExpirationTS`<" . (time() - $this->lifeTime));
            if (is_resource($r) && (($rows = mysql_num_rows($r)) !== 0)) {
                for ($i=0;$i<$rows;$i++) {
                    $this->destroy(mysql_result($r,$i,"sessionID"));
                }
            }

            return true;
        }
    }

    ini_set("session.gc_maxlifetime",60 * 30); # 30 minutes
    session_set_cookie_params(0,"/",".myapp.com",false,true);
    session_name("MYAPPSESSION");
    $sessionHandler = new SessionHandler();
    session_set_save_handler(array (&$sessionHandler,"open"),array (&$sessionHandler,"close"),array (&$sessionHandler,"read"),array (&$sessionHandler,"write"),array (&$sessionHandler,"destroy"),array (&$sessionHandler,"gc"));
    session_start();
?>

And here’s the SQL for the DB portion of the Session Handler:


CREATE TABLE `tblsessions` (
    `sessionID` VARCHAR(32) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
    `sessionExpirationTS` INT(10) UNSIGNED NOT NULL,
    `sessionData` TEXT COLLATE utf8_unicode_ci NOT NULL,
    PRIMARY KEY (`sessionID`),
    KEY `sessionExpirationTS` (`sessionExpirationTS`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

As always, I welcome any and all CONSTRUCTIVE criticism. If you have something to add, or a bug fix or any suggestions I fully welcome them here. Trolls need not apply.

Make a Comment

Make a Comment: ( 15 so far )

blockquote and a tags work here.

15 Responses to “Memcache & MySQL PHP Session Handler”

RSS Feed for Adventures in PHP / DHTML / CSS and MySQL Comments RSS Feed

This may work for very simple use case but since you are not handling transaction failures there is the potential for the cache to become inconsistent – which is probably not so good for anything a little more complex.

You forgot:

register_shutdown_function('session_write_close');

If you don’t use this, php will try to write and close session after it destroyed the handler objects.

Excellent. I’ve been looking for some code to do this for a while, since I read the post sggesting the same thing at http://dormando.livejournal.com/495593.html

I’ve not run the code, but my one immediate suggestion is to use some kind of dependency injection (for the Memcache and mysql) to maybe aid in testing. Most people will also have their own pre-built memcache and DB access layers, so they’d have to tweak it out themselves, but this is probably 90% of the work done.

why not also populate the cache after a read fell through to the db?

Thanks for the suggestion :-) … and for the awesome book, too

hi,

some hints for readability and performance…

Returning true from __construct seems pretty pointless.

$sh = new SessionHandler(); // memcache->get($sessionID);
- if ($data === false)
+ if ($data)
+ return $data; // return on positive match!
+ //You save an indentation level and you don’t have to worry about the following

# Couldn’t find it in MC, ask the DB for it
$sessionIDEscaped = mysql_real_escape_string($sessionID);
- $r = mysql_query(“SELECT `sessionData` FROM `tblsessions` WHERE `sessionID`=’$sessionIDEscaped’”);
+ $query = <<lifeTime;
+ $query = <<<EOSQL
+SELECT
+ sessionID
+FROM
+ tblsessions
+WHERE
+ sessionExpirationTS < {$expTime}
- $r = mysql_query(“SELECT `sessionID` FROM `tblsessions` WHERE `sessionExpirationTS`lifeTime));
+ $result = mysql_query($query);
- if (is_resource($r) && (($rows = mysql_num_rows($r)) !== 0)) {
+ if (!$result)
+ return true; // is it necessary to return true always?! seems pretty pointless..
- for ($i=0;$idestroy(mysql_result($r,$i,”sessionID”));
+ $this->destroy($sessionId);

eeekk,

the indentation is broken in the above post. Some things are wrong there….
If someone wants the correct version email me!

Is a disk or LRU failure of memcache really so common as to need a write-through for session data? I thought the idea of this approach (and we use it a lot) is for non-volatility?

Seems the problem is far simpler if you just allocate a dedicated pool of memcaches for sessions, set the session.handler, and let the chips fall where they may on failure.

@alvin Memcache is so fast (and this being session data which is usually accessed serially), the probability of this is actually pretty small. If consistency is needed, the memcached extension has a feature for preventing writes unless the data is consistent.

[...] the Pureform WordPress blog is a quick tutorial on using memcache and MySQL to work with PHP’s session handler to create a Write Through [...]

[...] the Pureform WordPress blog is a quick tutorial on using memcache and MySQL to work with PHP’s session handler to create a Write Through [...]

Just a few tips, split the underlying storage out using a factory/registry, set the session save handler etc within an initialization method. Take a look at Caching in PHP using the filesystem, APC and Memcached as an example.

Also the garbage collection has the potential to cause your database to fallover by simply using session fixation with a simple DOS attack.

Check out this fix for avoiding session fixation, that will help the problem with DB overloading with a DOS attack

http://devzone.zend.com/article/1786-PHP-Security-Tip-7

With the simple session_regenerate_id(true) function, it will patch up that error

[...] Memcache & MySQL PHP Session Handler [...]

seems like you’d still be setting it on every page.. ‘cept now you’re doing twice the work. I don’t see any gain here. Flickr stuffs all their “session” data into cookies, not memcache. stateless and scales in every direction.

The gain is the possibility of scaling to multiple servers. You can’t use the default session settings if you have multiple servers as the session files are written to disk, unless you are using a shared volume which is mounted on all your servers.

You need to use something that all servers uses, a database or memcached or database+memcached are good examples. PHP Session support memcache handler but having it in the database also allows you to fallback on the database if Memcached goes down.

Why using Database+Memcached? To avoid asking the database for information that can be cached.

I don’t know how Flickr does it, but I am sure they are not using only cookies to authenticate their users as cookies can be alter quite easily.

I hope that helps.


Where's The Comment Form?

Liked it here?
Why not try sites on the blogroll...