Ars Longa, Vita Brevis

Подход, позволяющий хранить PHP-сессии в базе данных вместо файлов

Начну сразу с причин, по которым я пишу эту статью. Я периодически просматриваю лог запросов, по которому люди попадают сюда, и вот один из запросов — хранить php сессию в mysql.

Итак, как же хранить PHP-сессии в базе данных?

На самом деле, в этом нет ничего сложного: в PHP есть одна полезная функция — session_set_save_handler — которая и выполняет всю грязную работу:

session_set_save_handler() sets the user-level session storage functions which are used for storing and retrieving data associated with a session. This is most useful when a storage method other than those supplied by PHP sessions is preferred. i.e. Storing the session data in a local database.

Эта функция принимает шесть аргументов типа сallback:

  • open — вызывается при открытии/создании сессии;
  • close — вызывается при закрытии сессии (например, чтобы приложение могло закрыть файл);
  • read — чтение сессии;
  • write — запись сессии;
  • destroy — уничтожение сессии (session_destroy);
  • gc — "сборка мусора"

Все функции (кроме read) должны возвращать true, если все прошло успешно и false в противном случае.

Для хранения сессии у нас есть такая таблица:

[-]
View Code MySQL
CREATE TABLE `session` (
    `id` CHARACTER(32) BINARY NOT NULL PRIMARY KEY, /* Session ID */
    `expires` INTEGER NOT NULL, /* Время истекания сессии */
    `session_data` TEXT NOT NULL, /* Данные, хранящиеся в сессии */

    KEY(`expires`)
)

Один маленький нюанс: если в MySQL делать таблицу сессий типа MEMORY (т.е. задать ей такой storage engine), то могут возникнуть проблемы с удалением записей при сборке мусора, ибо MEMORY storage engine не может использовать индексы для операций сравнения типа "больше"/"меньше".

Для абстракции от конкретного SQL-сервера, будем считать, что у нас имеется некий класс Persistent, обладающий способностями загружать/сохранять данные в базу данных.

Таким образом, реализация класса Session будет иметь следующий вид:

[-]
View Code PHP
<?php
    require_once('class.Persistent.php');

    class Session extends Persistent
    {
        /**
         * @var string
         */

        public $id;

        /**
         * @var int
         */

        public $expires;

        /**
         * @var string
         */

        public $session_data;

        /**
         * @var bool
         */

        public $m_new;

        function __contruct($data)
        {
            parent::__construct($data);
        }

        static function getTable()
        {
            return TABLE_SESSION; //константа, задающая имя таблицы сессий
        }
    }
?>

Пока все предельно просто. О переменной $m_new поговорим позже.

Теперь собственно реализация класса, управляющего сессиями:

[-]
View Code PHP
<?php
    require_once('class.Session.php');

    class SessionManager
    {
        /**
         * @var int
         */

        protected $life_time;

        /**
         * @var Session
         */

        protected $session;

        /**
         * @return SessionManager
         */

        public static function& instance($reinit = false)
        {
            static $self = null;

            if (true == is_null($self)) {
                $self = new SessionManager();
                $reinit = true;
            }

            if (true == $reinit) {
                session_set_save_handler(
                    array(&$self, "open"),
                    array(&$self, "close"),
                    array(&$self, "read"),
                    array(&$self, "write"),
                    array(&$self, "destroy"),
                    array(&$self, "gc")
                );

                register_shutdown_function('session_write_close');
            }

            return $self;
        }

        public function open($save_path, $sess_name)
        {
            $this->life_time = intval(get_cfg_var('session.gc_maxlifetime'));
            $this->session = new Session(
                array(
                    'id'           => (true == isset($_COOKIE[$sess_name])) ? $_COOKIE[$sess_name] : session_id(),
                    'expires'      => time() + $this->life_time,
                    'session_data' => '',
                )
            );

            $this->session->m_new = true;
            return true;
        }

        public function close()
        {
            return true;
        }

        public function read($sid)
        {
            $this->session = Session::load('Session', $sid, 3600);
            if (false == $this->session instanceof Session) {
                $this->session = new Session(
                    array(
                        'id'           => (true == isset($_COOKIE[session_name()])) ? $_COOKIE[session_name()] : session_id(),
                        'expires'      => time() + $this->life_time,
                        'session_data' => '',
                    )
                );

                $this->session->m_new = true;
            }
            else {
                $this->session->m_new = false;
            }

            return (string)$this->session->session_data; //Явное приведение типа позволит избежать трудноуловимых ошибок
        }

        public function write($sid, $data)
        {
            $this->session->m_new        |= ($sid != $this->session->id);
            $this->session->id            = $sid;
            $this->session->session_data  = $data;
            $this->session->expires       = time() + $this->life_time;

            $mode = (true == $this->session->m_new) ? SAVE_INSERT : SAVE_UPDATE;
            $this->session->save($mode);

            return true;
        }

        public function destroy($sid)
        {
            unset($_COOKIE[$sid]);
            Session::deleteMany('Session', new QueryCondition(array('id' => $sid)));
            return true;
        }

        public function gc($max_time)
        {
            Session::deleteMany('Session', new QueryCondition("`expires` < '" . time() . "'"));
            return true;
        }

        /**
         * @return Session
         */

        public function& getSession()
        {
            return $this->session;
        }
    }
?>

Пример использования:

[-]
View Code PHP
<?php
    $sm = &SessionManager::instance();
    session_start();
?>

Вкратце о $m_new. Дело в том, что ID сессии при ее открытии может не сопадать с ID при закрытии (иными словами, измениться в ходе выполнения скрипта). Один из способов — это использование функции session_regenerate_id. Поэтому при сохранении сессии важно знать, является ID новым или нет (если ID новый, то для сохранения будет использоваться INSERT, если существующий — то UPDATE; по большому счету можно обойтись одним REPLACE, однако это не особо эффективное решение). Для этого при открытии сессии получаем текущий идентификатор сессии и сравниваем его с тем, который получаем при сохранении сессии (конечно, в этом простом случае можно было обойтись без лишней переменной, но в более сложных проектах она может понадобиться); в зависимости от их равенства/неравенства используем тот или иной метод сохранения данных.

Строка register_shutdown_function('session_write_close') гарантирует, что сессия будет сохранена (без нее в PHP4 у меня были случаи, когда сессия не сохранялась).

О том, как прочитать данные из Session::session_data, можно прочитать в этой статье. Сразу отмечу, что PHP самостоятельно выполняет восстановление данных, которые ему передаются функцией SessionManager::read(), просто иногда бывают ситуации, когда сессию нужно восстановить вручную (подпатчить на лету).

При более глубоком изучении вопроса также будет полезна эта замечательная статья.

Добавить в закладки
  • del.ici.ous
  • Digg
  • Furl
  • Google
  • Simpy
  • Spurl
  • Y! MyWeb
  • БобрДобр
  • Мистер Вонг
  • Яндекс.Закладки
  • Текст 2.0
  • News2
  • AddScoop
  • RuSpace
  • RUmarkz
  • Memori
  • Закладки Google
  • Писали
  • СМИ 2
  • Моё Место
  • Сто Закладок
  • Ваау!
  • Technorati
  • RuCity
  • LinkStore
  • NewsLand
  • Lopas
  • Закладки - I.UA
  • Connotea
  • Bibsonomy
  • Trucking Bookmarks
  • Communizm
  • UCA

Комментарии к статье "Хранение PHP-сессий в базе данных" (7) »

  1. [Апрель 23, 2008 5:20 пп] Dok:

    Оч нужная статья, но ничерта не ясно…

    #1
  2. [Апрель 23, 2008 5:36 пп] Vladimir:

    Dok, задавайте вопросы, будем разбираться… На самом деле всё не так страшно, как оно кажется :-)

    #2
  3. [Май 10, 2008 6:06 пп] mp3 мелодии:

    Не че не получилось. Будем разбираться сами!
    Тем немениее спасибО!

    #3
  4. [Июль 14, 2008 11:32 пп] m104:

    Автору респект!
    Собирался сам изобретать велосипед!:)

    #4
  5. [Июль 19, 2008 5:30 пп] babr:

    Еще одна подробная статья, выполненная на отличном уровне.

    Сам в свое время разбирался в этом.
    Очень хорошо, что теперь есть куда посмотреть и вспомнить если что :)

    Спасибо вам!

    С уважением, Савунов Василий (babr)

    #5
  6. [Сентябрь 16, 2008 2:03 дп] андрей:

    А какие приемущества хранить сессии в базе перед файлами?

    #6
  7. [Сентябрь 16, 2008 2:18 дп] Vladimir:

    Самые разные… :-)

    Например, если Вам нужно посчитать количество пользователей в онлайне. ID всех сессий априорно Вы знать не можете, а с БД такой запрос — пара пустяков: SELECT COUNT(DISTINCT `user_id`) FROM `sessions` WHERE `user_id` != 0.

    Прибить сессию (сессии) тоже проще через БД.

    Или, допустим, сайт знакомств: Вам нужно найти пользователей, удовлетворяющих каким-то условиям и находящихся сейчас на сайте. С БД это проще.

    #7

RSS лента комментариев к этой записи. TrackBack URL

Оставить комментарий к записи "Хранение PHP-сессий в базе данных"

XHTML: Вы можете использовать эти теги: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Оставляя комментарий, Вы выражаете своё согласие с Правилами комментирования.

Подписаться, не комментируя