Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

Клуб профессиональных программистов :: Выпуск #9




Сессии PHP. Часть 5.
Хранилище. Хранение в БД.

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

Для начала создадим таблицу для хранения сессий.

CREATE TABLE sessions
(
    sid CHAR(32) NOT NULL PRIMARY KEY,
    atime TIMESTAMP,
    data TEXT NOT NULL DEFAULT ''
);

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

  1. sid

    Это поле будет первичным ключом - в нем будет храниться идентификатор сессии. Размер я выбрал, исходя из стандартного для PHP размера идентификатора, вычисленного по функции md5 - 32-е hex-цифры.

    В PHP5 появилась возможность выбирать функцию, генерирующую идентификатор сессии - md5 или sha1. Функция sha1 создает более длинную строку (40 hex-цифр) - это нужно учитывать. Хеш-функция определяется в конфиге параметром session.hash_function, или в программе посредством ini_set(). Подробнее можно прочесть здесь: http://www.php.net/manual/ru/ref.session.php .

    Возможно, кому-то захочется ради оптимизации переводить hex-строку в бинарное представление. На этот случай напоминаю, что для md5 это будет 16 байт, а для sha1 - 20. Числовых типов такой размерности в mysql нет, и придется хранить идентификатор либо в CHAR BINARY, либо разбивать его на несколько полей (например, по 4 байта для типа INT) и объединять их все в одном индексе.

  2. ...
        sid0 INT NOT NULL,
        sid1 INT NOT NULL,
        sid2 INT NOT NULL,
        sid3 INT NOT NULL,
     ...
        PRIMARY KEY (sid0,sid1,sid2,sid3)
    

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

  3. atime

    Это поле будет содержать время последнего обращения к записи. Для него я выбрал тип TIMESTAMP из-за его интересных свойств:

    1. при вставке в него NULL будет вставлено текущее время;
    2. при модификации любого поля в строке первому полю TIMESTAMP будет присвоено текущее время.

    Замечу, что под "текущим" подразумевается время сервера MySQL. Если программа работает не на том же сервере, что и MySQL, то время может различаться. Поэтому в sql-выражениях в качестве источника текущего времени я использовал функцию NOW(), хотя мог вычислить нужное значение в PHP. Просто так надежнее.

  4. data

    Это поле будет содержать сериализованную строку данных сессии. Я посчитал, что типа VARCHAR может не хватить (в указанным мной версиях оно ограничено 255 символами) и выбрал тип TEXT - 65535 байт должно хватить.

Теперь пройдемся по функциям.

Как я уже упоминал в предыдущей части, подключиться к БД логичнее в начале программы, а не в функции open. Поэтому функции open и close нам не нужны, но так как обработчик не может состоять из меньшего числа функций, их реализую пустыми. От них требуется только вернуть true.

В функции read нужно попытаться прочесть строку из БД. Если строки нет, то ее можно создать сейчас, либо позже - в функции write. Я предпочел сделать это сразу.

Чтобы за время между чтением и записью данных сессии сборщик мусора не удалил эту строку (хоть и маленькая, но вероятность есть), я добавил обновление atime в строке.

В функции записи обновляю строку, занося новые данные и обновляя время доступа. Если строки не будет, ошибки не произойдет, но и данные не запишутся. По этому стоит проверить, сколько строк было обновлено, и если ни одной, то вставить ее заново. Обе задачи так же можно решить посредством оператора REPLACE.

Функция delete проста, и обработка ошибок не нужна. Просто удаляю строку с указанным идентификатором. И не важно, была она или нет.

Функция gc похожа на delete. Только удаляю не по идентификатору, а по времени доступа с учетом времени жизни.

Вот код примера. Аналогично предыдущей части я сделал его в виде класса.

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

А теперь прощаемся с Вами до следующего выпуска.

                                        С уважением, команда Клуба.




В избранное