ФорумПрограммированиеПыхнуть хотите?F.A.Q. → Сессии в memcached

Сессии в memcached

  • vasa_c

    Сообщения: 3127 Репутация: N Группа: в ухо

    Spritz 21 февраля 2009 г. 8:43

    Сегодня дорогие читатели поговорим о сессиях :)

    Нужно реализовать на сайте 3 вещи:
    1. Отслеживание сессий (сеансов) пользователей.
    2. Отслеживание онлайн-пользователей.
    3. Чтобы всё это работало под более-менее внушительной нагрузкой.

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

    Самое простое решение:
    1. Стандартный механизм сессий в PHP.
    2. Завести в таблице пользователей поле `last_visit` и при каждом заходе пользователя обновлять его на текущее время. У кого разница между текущим временем и last_visit меньше, допустим, 5 минут, тот считается "на сайте".

    Стандартные пыхосессии не любил никогда и всегда реализовывал по своему. Однако, при больших нагрузках здесь дело уже не в любви и вкусе. Сессии в виде файлов, сваленных в один каталог, считываемых и изменяемых (с блокировками!) при каждом запросе страницы, потянут нагрузку только до определённого уровня. А дальше не потянут.

    Поэтому нужно либо переопределять стандартные механизмы (session_set_save_handler), либо делать свои. Здесь будем делать свои :)

    Первым делом на ум приходит - пихать сессии в базу. Однако и база может обидется на такое отношение к себе.

    У сайта под большими нагрузками по любому должен быть всеми нами любимый Мемкэш. Вот в него родимого и будем пихать.

    [size=12]Создание/загрузка сессии[/size]

    При создании сессии, обычно, создаётся sid (идентификатор сеанса), с ним связываются какие-то данные (данные сессии) на сервере, а сам он отправляется клиенту в виде куки.
    Приходит от клиента кука с сидом - грузим по ней сессию.

    Создаём сессию:

    $userId = 5; // После проверки формы авторизации, оказалось, что к нам пришёл юзер с ID=5.
    $sid = "Уникальный хэш рассчитанный по хитрожопому алгоритму";
    setCookie("sid", $sid, 0, "/"); // Сессионная кука (удаляется при закрытии браузера).
    $memVarName = "ses_".$sid;
    $sessionData = Array($userId, time());
    $memcached->set($memVarName, $sessionData, SESSION_EXPIRATION); // Сохраняем в мемкэше


    В мемкэше сохраняем переменную с именем завязанным на sid (с префиксом "ses_").
    Так как далеко не все пользователи настолько сознательны, чтобы всегда нажимать на "exit", ставим переменной время устаревания (SESSION_EXPIRATION), допустим 30 минут.

    В качестве данных сессии ($sessionData) можно передавать любою структуру. В том числе и любимые многими "переменные сессии" (но не забывать при работе с ними, что в memcached нет блокировок).
    В общем случае же нам нужны только два параметра - ID авторизованного пользователя и время последнего обновления сессии (о нём ниже).


    Загрузка же сессии происходит следующим образом:

    /* К нам пришла кука sid */
    $sid = $_COOKIE['sid'];
    $memVarName = "ses_".$sid;
    $sessionData = $memcached->get($memVarName);
    if ($sessionData) {
    $userId = $sessionData[0]; // Авторизованный пользователь
    } else {
    echo "Нету такой сессии!";
    }


    [size=12]Пользователи on-line[/size]

    Приведённый в начале статьи вариант с обновляемым полем `users`.`last_visit` не жизнеспособен в наших условиях.
    Если у нас сотни запросов к сайту в секунду, то будут сотни запросов к таблице на обновление. А в таблице миллионы пользователей, а на last_visit висит индекс (нужно выбирать он-лайн пользователей). Всё ляжет. Да даже запрос количество пользователей на сайте совсем не обрадует базу.
    SELECT COUNT(*) FROM `users` WHERE NOW() - `last_visit` < Interval 5 minute


    При создании сессии, создаём ещё переменную в мемкэше:

    $memVarName = "u_online_".$userId;
    $memcached->set($memVarName, 1, SESSION_EXPIRATION);

    Значение переменной не важно, важен сам факт её наличия. Ставим то же время устаревания и сведения о том что пользователь на сайте умрёт вместе с сессией.

    И проверка, на сайте ли пользователь:

    function userIsOnline($userId)
    {
    return (bool)$memcached->get("u_online_".$userId); // Откуда брать $memcached на задачу не влияет
    }


    Мемкэш в данном случае позволяет по ID пользователя получать информацию о том на сайте ли он. Это удобно для отображения на странице пользователя или в списках пользователей.

    Что мемкэш не позволяет? Не позволяет делать выборки.
    1. Нельзя получить общее количество пользователей на сайте.
    2. Нельзя выбрать их.
    3. Нельзя делать более сложные выборки - например, "мои друзья on-line", как на vkontakte. Есть вариант - выбрать всех друзей и перебрать всех, онлайн ли они или нет. И база не обрадуется и мемкэш при всей его скорости, всё же медленее скорости света.

    Поэтому таки вспоминаем про базу и создаём в ней таблицу:

    CREATE TABLE `users_online` (
    `user` INT UNSIGNED NOT NULL,
    `last` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`user`),
    KEY (`last`)
    ) ENGINE=MEMORY;


    Эти данные не являются критическими и, с учётом того, что табличка достаточно легкая (всё же онлайн-пользователей намного меньше общего количества и тут на них выделяется только по два INT'а + индексы), так что делаем её MEMORY, что значительно ускоряет всё дело.

    Ну а дальше всё, как с обычными таблицами - жойним с чем нужно, выбираем COUNT(*) и т.п.

    По CRON'у удаляются устаревшие записи (всё тот же SESSION_EXPIRATION).

    [size=12]Синхронизация[/size]

    Таблица должна синхронизироваться с мемкэшем. При создании сессии, создаётся запись в ней.
    Но совершенно не обязательно проводить синхронизацию при каждой загрузке страницы (слишком жирно).

    Делаем так (при загрузке сессии):

    $sid = $_COOKIE['sid'];
    $sessionData = $memcached->get("ses_".$sid);
    if (!$sessionData) {
    die('Вот фак!');
    }
    $userId  = $sessionData[0];
    $lastUpd = $sessionData[1];
    if (time() - $lastUpd > 60) {
    $sessionData[1] = time();
    $memcached->replace("ses_".$sid, $sessionData, SESSION_EXPIRATION);
    $memcached->increment("u_online_".$userId);
    $db->query("REPLACE INTO `users_online` (`user`) VALUES ($userId)");
    }


    То есть особо рьяный пользователь нащёлкал за минуту десять страниц, но обновление произошло только один раз.
    Так же и обновляем мемкэш-переменные - это нужно для продление их жизни.

    [size=12]Последний визит[/size]

    `users`.`last_visit` всё же полезна для статистики и надписей вида "Вася был на этом дурацком сайте свыше года назад и больше его здесь не видели".

    Но опять-таки не нужно обновлять его каждый раз.

    По CRON'у раз в полчасика:
    UPDATE `users` INNER JOIN `users_online` ON `user_id`=`user` SET `last_visit`=`last`


    [size=12]Итог[/size]

    Как и обычно всё вышесказанное является туманным описанием одного из вариантов и не отменяет необходимости думать над своим проектом своей же головой :)
  • adw0rd

    Сообщения: 22905 Репутация: N Группа: в ухо

    Spritz 21 февраля 2009 г. 17:01, спустя 8 часов 18 минут 43 секунды

    Класс! молодец! :D
    adw/0
  • AlexB

    Сообщения: 4290 Репутация: N Группа: в ухо

    Spritz 22 февраля 2009 г. 2:43, спустя 9 часов 41 минуту 54 секунды

    Хорошая статья. Плюсанул. ))))

    Для полноты картины могу дополнить, что PHP имеет стандартный механизм для хранения данных сессии в мемкеш. Он может быть полезен для того, чтобы иметь общую сессию для сайта, располагающегося физически более чем на одной машине. Однако и здесь надо помнить про, упомянутое в статье, отсутствие блокировок.
  • Z-MODe

    Сообщения: 199 Репутация: N Группа: Кто попало

    Spritz 28 февраля 2009 г. 8:41, спустя 6 дней 5 часов 57 минут

    Мега!
  • krasun

    Сообщения: 1370 Репутация: N Группа: Джедаи

    Spritz 28 февраля 2009 г. 14:34, спустя 5 часов 53 минуты 31 секунду

    memcache & memcached
  • vasa_c

    Сообщения: 3127 Репутация: N Группа: в ухо

    Spritz 28 февраля 2009 г. 15:44, спустя 1 час 9 минут 50 секунд

    krasun, что, где?
  • krasun

    Сообщения: 1370 Репутация: N Группа: Джедаи

    Spritz 28 февраля 2009 г. 23:14, спустя 7 часов 29 минут 58 секунд

    не это я так, просто вспомнил, что есть memcache, а есть memcached
  • adw0rd

    Сообщения: 22905 Репутация: N Группа: в ухо

    Spritz 1 марта 2009 г. 8:44, спустя 9 часов 29 минут 45 секунд

    krasun, memcache это технология, а memcached это сервер… где "d" это демон.
    adw/0
  • krasun

    Сообщения: 1370 Репутация: N Группа: Джедаи

    Spritz 1 марта 2009 г. 16:06, спустя 7 часов 21 минуту 59 секунд


    krasun, memcache это технология, а memcached это сервер… где "d" это демон.


    Странно.

    http://ua.php.net/manual/ru/book.memcached.php

    http://ua.php.net/memcache

  • krasun

    Сообщения: 1370 Репутация: N Группа: Джедаи

    Spritz 1 марта 2009 г. 16:06, спустя 48 секунд

    adw0rd, вы были правы
  • adw0rd

    Сообщения: 22905 Репутация: N Группа: в ухо

    Spritz 1 марта 2009 г. 17:59, спустя 1 час 52 минуты 19 секунд

    krasun, у PHP это просто два расширения для работы с демоном мемкеша
    adw/0
  • vasa_c

    Сообщения: 3127 Репутация: N Группа: в ухо

    Spritz 3 июня 2009 г. 4:51, спустя 93 дня 9 часов 52 минуты

    Немного лажанулся.

    Синхронизация последнего визита по CRON'у:
    UPDATE `users` SET `last_visit` = (SELECT `last` FROM `users_online` WHERE `user_id`=`user`)

    так для всех юзеров, которым не соответствует запись в `users_online` (не на сайте), `last_visit` убьётся в NULL.

    Нужно что-то вроде:
    UPDATE `users` INNER JOIN `users_online` ON `user_id`=`user` SET `last_visit`=`last`

    ну и желательно смотреть чтобы при этом запросе перебирался не большой `users`, а `users_online`.

    PS. Спасибо ghost'у за подсказки


Пожалуйста, авторизуйтесь, чтобы написать комментарий!