Memcache & MySQL PHP Session Handler

Posted on April 8, 2009. Filed under: Memcache, mysql+, PHP | 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.

About these ads

Make a Comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

35 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.

Why not using ENGINE=MEMORY?

Majk!

Can you post the right code?

[...] session handler can be found at Php.net . Another memcache based session handler can be found at PureForm . There’s plenty of code around to get the job done. [...]

[...] http://pureform.wordpress.com/2009/04/08/memcache-mysql-php-session-handler/ This is protected content. Please Login or Register for access.ช่วงนี้กำลังหาโซลูชั้นเพื่อที่จะทำ centralized session ตรงกลาง เพราะเนื่องจากเริ่มมี web application หลายตัว และในบางส่วนจำเป็นต้องมีการใช้ session ร่วมกัน จึงหาการแก้ปัญหาที่ตอบโจทย์ดังกล่าวว่าจะมีการรวม session มาไว้ที่เดียวกันและมีประสิทธิภาพสูงสุดได้อย่างไร ซึ่งในโซลูชั่นสุดท้ายผมคงต้องขอไม่พูดถึง เพี่อเป็นความปลอดภัยของระบบ แต่จะกล่าวแค่เฉพาะแนวคิดเฉยๆ ครับ การทำ centralized session หรือ เซสชั่นกลางนั้น เราสามารถทำได้ง่ายๆ โดยการหาตัวมารับหน้าที่เป็น data pool หรือที่เก็บรวบรวมข้อมูลตรงกลาง ซึ่งที่ผมจะนำไปใช้คือ MySQL สำหรับ บรรจุข้อมูลตรงกลาง ทุกเครื่องสามารถนำไปใช้งานได้ โดยเป็นข้อมูลเดียวกัน แต่เนื่องจาก MySQL จะทำการ update cache ทุกครั้งที่กำการอัพเดตข้อมูล [...]

it does not handle a concurrent writes for same session id…

What if, i have multiple domain like google.com, gmail.com etc.
Will it work?
session_set_cookie_params(0,”/”,”google.com,gmail.com”,false,true);

How do i access sessions on gmail.com which is defined on google.com?

Nice post, thank you.

But I only managed to set the session handlers after calling “session_write_close()”. Without this, I couldn’t set my own handlers.

$sessionHandler = new SessionHandler();
session_write_close();
session_set_save_handler(…

Thanks again!

Hi, you might be interrested in our working solution, very much inspired by this original post: http://www.keboola.com/blog/php-sessions-with-memcached-and-a-database-session-in-the-cloud-done-right/

Great, I’ll have to take a look! :-)

BTW, I ended up going with _just_ memcache for sessions due to it being much too I/O intensive with MySQL. That implementation is very similar to this, just without the db calls.

We tried tha and it’s described in the article as a wron approach. Memcache-only won’t definitely work and once your machine gets into some load, you’ll begin to loose sessions.

Actually that memcached and database solution is not done completely right. Multiple servers with simultaneous access to same $_SESSION data overwrite each other’s modifications.

$_SESSION on multi frontend installation is broken by design without serializing access to it by locking. And locking $_SESSION so that only one server can modify it at time is not easy thing to do. I would suggest instead to store and access data as small fragments related only to currently running scripts activity. By this way other simultaneous scripts can access freely other paths related to their operations. This was not so big problem on traditional web pages, but ajax apps that burst multiple requests in a row to wire might end up having separate front end servers serving their requests and racing for session data.

Try SCache at http://scache.nanona.fi/

It is specifically written to accessing data this way on clustered environments.

You can set override the session handler in the constructor function like

session_set_save_handler(
array(&$this, ‘open’),
array(&$this, ‘close’),
etc…
);

Hi,

This script is nice, but can you also post a version that is non-memchached ?

Would be great to have!

Can you be a bit more specific?

Hi, It seems that it’s better to use memcache as you have developed this.

In which way do you connect ? Does this need some extra memcache based scripting too ?

No you don’t. You just need to have the Memcache extension installed. However, I would use the “Memcached” extension and not the “Memcache”, these are two different extensions. As a personal preference, I would not use memcached at all, I would use Redis instead.

Thanks, I know I have this installed, but I thought you might need some mysql connect that also is understood by Memcached/Memcache. What kind of connect do you use for this script? Basic one, or something else ?

I have read about Redis, but does this work with this code also ? Redis seems to be much smaller in footprint.

Redis/Memcached does not speak MySQL, you have to do that yourself. If you are looking for a Redis solution, do a google search for “redis php session”, you’ll find two github repo that are a good starting point.

Hey,
Thanks for the code, exactly what I was looking for.
Please note though, that I’ve used this code and I found a critical bug that caused all my servers to be stuck for almost an hour:
The “read” function of a session handler must return an empty string – not false! otherwise PHP will overload and crash. You can see this demand on php.net.
Meaning that you should replace:
# The default miss for MC is (bool) false, so return it
return $data;

With:

// Return empty string when session is not found
return ”;

doesn’t work for me, causes internal 500 server errors, shame as i’d really like to get this class to work.


Where's The Comment Form?

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

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: