Нужно реализовать на сайте 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]
Как и обычно всё вышесказанное является туманным описанием одного из вариантов и не отменяет необходимости думать над своим проектом своей же головой :)