Статья
Версия для печати
Обсудить на форуме
Сессии PHP. Часть 5.


Хранилище. Хранение в БД.

Еще раз об обработчиках. В этот раз рассмотрим обработчик сессии, хранящий данные в базе данных.
СУБД я выбрал наиболее распространненую в web-серверах - MySQL версии 3.23 или 4.0. Соответственно, все ниженаписанное относится к MySQL и при использовании другой СУБД без доработки может не работать.
Для начала создадим таблицу для хранения сессий.

Код: (SQL)
CREATE TABLE sessions
(
    sid CHAR(32) NOT NULL PRIMARY KEY,
    atime TIMESTAMP,
    DATA TEXT NOT NULL DEFAULT ''
);

Рассмотрим поля подробнее:

  • sid
    Это поле будет первичным ключом - в нем будет храниться идентификатор сессии. Размер я выбрал, исходя из стандартного для PHP размера идентификатора, вычисленного по функции md5 - 32-е hex-цифры.
    В PHP5 появилась возможность выбирать функцию, генерирующую идентификатор сессии - md5 или sha1. Функция sha1 создает более длинную строку (40 hex-цифр) - это нужно учитывать. Хеш-функция определяется в конфиге параметром session.hash_function, или в программе посредством ini_set(). Подробнее можно прочесть здесь: http://www.php.net/manual/en/ref.session.php .
    Возможно, кому-то захочется ради оптимизации переводить hex-строку в бинарное представление. На этот случай напоминаю, что для md5 это будет 16 байт, а для sha1 - 20. Числовых типов такой размерности в mysql нет, и придется хранить идентификатор либо в CHAR BINARY, либо разбивать его на несколько полей (например, по 4 байта для типа INT) и объединять их все в одном индексе.

Код: (SQL)
...
    sid0 INT UNSIGNED NOT NULL,
    sid1 INT UNSIGNED NOT NULL,
    sid2 INT UNSIGNED NOT NULL,
    sid3 INT UNSIGNED NOT NULL,
...
    PRIMARY KEY (sid0,sid1,sid2,sid3)

При составлении sql-запроса также придется переводить идентификатор сессии в бинарное представление, нарезать на части и сравнивать со всеми sid полями. Не очень удобно - лучше уж CHAR BINARY.

  • atime
    Это поле будет содержать время последнего обращения к записи. Для него я выбрал тип TIMESTAMP из-за его интересных свойств:
    а) при вставке в него NULL будет вставлено текущее время;
    б) при модификации любого поля в строке первому полю TIMESTAMP будет присвоено текущее время.
    Замечу, что под "текущим" подразумевается время сервера MySQL. Если программа работает не на том же сервере, что и MySQL, то время может различаться. Поэтому в sql-выражениях в качестве источника текущего времени я использовал функцию NOW(), хотя мог вычислить нужное значение в PHP. Просто так надежнее.
  • data
    Это поле будет содержать сериализованную строку данных сессии. Я посчитал, что типа VARCHAR может не хватить (в указанным мной версиях оно ограничено 255 символами) и выбрал тип TEXT - 65535 байт должно хватить.

Теперь пройдемся по функциям.
Как я уже упоминал в предыдущей части, подключиться к БД логичнее в начале программы, а не в функции open. Поэтому функции open и close нам не нужны, но так как обработчик не может состоять из меньшего числа функций, их реализую пустыми. От них требуется только вернуть true.
В функции read нужно попытаться прочесть строку из БД. Если строки нет, то ее можно создать сейчас, либо позже - в функции write. Я предпочел сделать это сразу.
Чтобы за время между чтением и записью данных сессии сборщик мусора не удалил эту строку (хоть и маленькая, но вероятность есть), я добавил обновление atime в строке.
В функции записи обновляю строку, занося новые данные и обновляя время доступа. Если строки не будет, ошибки не произойдет, но и данные не запишутся. По этому стоит проверить, сколько строк было обновлено, и если ни одной, то вставить ее заново.
Функция delete проста, и обработка ошибок не нужна. Просто удаляю строку с указанным идентификатором. И не важно, была она или нет.
Функция gc похожа на delete. Только удаляю не по идентификатору, а по времени доступа с учетом времени жизни.
Вот код примера. Аналогично предыдущей части я сделал его в виде класса.

Код: (PHP)
<?php
class session_db
{
    function session_db()
    {
        session_set_save_handler(
            array(&$this, 'session_open'),
            array(get_class($this), 'session_close'),
            array(&$this, 'session_read'),
            array(&$this, 'session_write'),
            array(&$this, 'session_delete'),
            array(&$this, 'session_gc')
            );
    }

    function session_open($path, $name)
    {
        return true;
    }

    function session_close()
    {
        return true;
    }

    function session_read($sid)
    {
        if (!mysql_query("UPDATE sessions SET atime=NOW() WHERE sid='$sid'"))
                die(mysql_error()); // Стоит проверить права на UPDATE

        if (!mysql_affected_rows()) // строки нет - надо создать
        {
            if (!mysql_query(
                "INSERT INTO sessions (sid) VALUES ('$sid')"
                ))
                die(mysql_error()); // Может, вставка запрещена?

            return ""; // Т.к. данных нет, то возвращаю пустую строку
        }

        $res = mysql_query("SELECT data FROM sessions WHERE sid='$sid'");
        if (!$res)
            die(mysql_error()); // Проблема с БД? Настройками балуемся?

        $row = mysql_fetch_row($res);
        mysql_free_result($res);

        return $row[0];
    }

    function session_write($sid, $data)
    {
        $data = mysql_escape_string($data); // Настоятельно рекомендую
        mysql_query("REPLACE INTO sessions (sid, atime, data) " .
            "VALUES ('$sid', NOW(), '$data')");
    }

    function session_delete($sid)
    {
        return mysql_query("DELETE FROM sessions WHERE sid='$sid'");
    }

    function session_gc($lifetime)
    {
        mysql_query("DELETE FROM sessions " .
            "WHERE atime < DATE_ADD(NOW(), INTERVAL -$lifetime SECOND)");
        // Для оптимизации atime не должно участвовать в вычислении выражения
    }
}

mysql_connect("host", "user", "pass") or die(myqsl_error());

mysql_select_db("my_db");

$storage = new session_db;

session_name("MY_SESSION_ID"); // Имя у сессии будет такое

session_start(); // Поехали...
?>

Пока все.
В следующей статье я рассмотрю вопросы взаимных блокировок.

Роман Чернышов (RXL) 12.05.2006
Версия для печати
Обсудить на форуме