Memcache & MySQL PHP Session Handler
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.
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.
alvin
May 3, 2009
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.
inf3rno
May 4, 2009
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.
Topbit
May 4, 2009
why not also populate the cache after a read fell through to the db?
cal
May 5, 2009
Thanks for the suggestion :-) … and for the awesome book, too
pureform
May 5, 2009
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);
…
Majk
May 5, 2009
eeekk,
the indentation is broken in the above post. Some things are wrong there….
If someone wants the correct version email me!
Majk
May 5, 2009
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.
terry chay
May 5, 2009
[...] 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 [...]
Pureform Blog: Memcache & MySQL PHP Session Handler | Cole Design Studios
May 6, 2009
[...] 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 [...]
Pureform Blog: Memcache & MySQL PHP Session Handler | Cole Design Studios
May 6, 2009
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.
Andrew Johnstone
May 9, 2009
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
Raul
May 17, 2009
[...] Memcache & MySQL PHP Session Handler [...]
網站製作學習誌 » [Web] 連結分享
May 9, 2009
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.
Boris Knows
July 10, 2009
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.
wedix
September 13, 2009