На днях, на Хабре была статья о том, как можно уменьшить нагрузку на БД, путем экономии ресурсов на обновлении счетчиков. Действительно, различного рода счетчики, например, счетчик просмотра статей, генерируют массу запросов типа UPDATE, при этом неся очень малое количество пользы для кого бы то ни было. Отказаться от счетчиков совсем тоже неправильно. Многим интересно, насколько читаем тот или иной материал. Для кого-то малое количество просмотров статьи станет поводом для того, чтобы не тратить свое драгоценное время на ее прочтение. Есть и другие считалочки, наличие которых еще более привлекательно. Как же быть?
Лично я использовал бы кэш, например, демон Memcached. И обновление строки в БД производил бы один раз на определенный промежуток времени, допустим — 10 минут. Потеря кэша в данном случае не очень критична. Мы ведь считаем не школьников, присутствующих на уроке и не населении страны. Мы не слишком расстроимся если мимо нас проскользнут какие-нибудь 10-100 итераций. Да и при нормальной работе сервера с демоном, потеря данных кэша достаточно редкое явление.
Тем не менее, если варианты есть, давайте их рассмотрим.
Что предложили на Хабре?
Автор упомянутого материала предложил способ, при котором никаких промежуточных хранилищ данных не требуется вообще. То есть мы обходимся без механизма кэширования и базе данных даем право на отдых. На самом деле, суть способа крайне проста.
Каждый раз, при просмотре страницы, используя генератор псевдослучайных чисел, мы генерируем число, например, в диапазоне от 1 до 100. Затем проверяем, равно ли это число 50-ти (можете взять любое другое, попадающее в выбранный диапазон). Если условие выполняется, то мы обновляем значение счетчика в БД на 100. Почему так?
Чисто теоретически, за сотню просмотров страницы, хотя бы один раз условие должно выполнится (полагаю, это уже вопрос теории вероятности, в которой лично я не силен). Если условие выполняется, то мы можем считать, что страница была просмотрена сто раз, значит можем обновить данные в базе.
Тесты
Мы заменили сотню UPDATE на один единственный. Неплохо, правда? Но насколько велика погрешность при такой организации? Тесты покажут :)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | $totalCounter = 0; $realCounter = 10000; for ($i = 0; $i < $realCounter; $i++) { $number = rand(1, 100); if ($number == 50) { $totalCounter++; } } $result = $totalCounter * 100; printf("Реальное значение: %d ", $realCounter); echo '<br />'; printf("Искусственное значение: %d (%d)", $totalCounter, $result); echo '<br />'; printf("Разница: %d", $realCounter - $result); |
Вот такой вот простенький тест. Запускаем цикл на n-ое количество итераций, в данном случае на 10 тысяч. Далее генерируем «случайное» число и производим проверку. В результате выполнения получаем три строки. Первая показывает сколько было итераций — живых просмотров страницы. Вторая — сколько раз выполнилось условие равенства случайного числа и числа, определенного нами. В скобках отражено произведение этого счетчика и множителя 100. Ну и третья строка — разница между реальным значением счетчика и нашим.
Если вам лень самостоятельно гонять этот тест, то вот таблица с моими результатами.
| Итераций | Выполнилось | Разница | Мин. | Макс. |
|---|---|---|---|---|
| 1 000 | 9.8 (980) | 20 | 100 | -500 |
| 10 000 | 120.6 (12 600) | 2 600 | 200 | -700 |
| 1 000 000 | 9977,6 (997 760) | 2 240 | 700 | 3500 |
| 100 000 000 | 9977,6 (997 760) | 2 240 | 700 | 3500 |
Для каждого числа итераций тест запускался по 5 раз. Во втором и третьем столбце приведены средние значения (поэтому во втором столбце числа с дробной частью). Два последних столбца отражают минимальную и максимальную погрешности, которые были получены.
На самом деле, результаты теста совершенно необъективны. Да и какая может быть объективность, если все зависит от случая. В таблице результаты одни. На деле же, при малом количестве итераций (читай «просмотров статьи») разница между реальной величиной и получившейся очень заметна. Попадались результаты, в которых искусственное значение было в три раза меньше реального (300 к 1000). В таблице наоборот — оно в 2 раза больше.
Чем больше итераций, тем менее заметна (значима) разница. Из этого напрашивается вывод о том, что подобный подход хорошо подходит для сайтов с высокой посещаемостью. Для малых ресурсов экономия ресурсов никак не оправдывает такие большие погрешности. Но в целом, какому маленькому сайту нужна экономия на таких простых запросах? А для крупного проекта эти погрешности ничего не значат.
Идея работает, причем очень хорошо. До того, как провести тесты, мне казалось, что погрешности будут совершенно недопустимыми, но, как видите, все в рамках разумного.
P.S: ссылку на хабротопик, который я здесь упоминал, я потерял. Если кто-то найдет его на хабре, оповестите и я обязательно на него сошлюсь.
Автор: Мурашов Олег
Источник: http://inroot.ru/

Да, при малых количествах итераций разница ОЧЕНЬ заметна. У меня получались еще большие диапазоны, нежели у вас.
Вот ссылка на хабр, если конечно еще нужна =)
http://habrahabr.ru/blogs/algorithm/70413/
За тесты спасибо большое, сейчас как раз встал вопрос об использовании подобной схемы. Может у вас уже есть какие-то несекретные нароботки по этому вопросу, чтобы сократить разброс значений? Буду непомерно благодарен =)
Спасибо!
Признаться, я на эту тему больше не думал, потому наработок никаких нет. Я в статье писал, что для посещаемых ресурсов, погрешности невелики. Можно добавить какой-нибудь механизм, который в начале каждых суток будет искусственно корректировать счетчики, например, на основании данных за прошлый день, но, имхо, это трата времени, которая ощутимых результатов не даст.
UDP: спасибо за ссылку. Добавлю ее в статью
Да, это пустая трата времени (с корректированием).
Я не очень силен в математике, но была идея сделать несколько, так скажем, “уровней” накрутки. Тоесть в зависимости от показания рандомного числа вызывать обновление счетчика в разный промежуток времени. Вот коммент интересный на эту тему http://habrahabr.ru/blogs/algorithm/70413/#comment_2012240 .
Для примера кусочек кода:
if(rand(0,9) == 5) {
// обновляем счетчик + 10
} elseif(rand(0,9) == 9) {
// обновляем счетчик + 5
}
это так, пример, хотя может и дурацкий :)
Не буду утверждать, но мне кажется, что на тестах будут такие же показатели погрешностей.
Тестов не делали?
Делал, погрешность иногда +/-5 на 100 итераций, а иногда и того больше (за 50). Наверняка есть какая-то золотая середина, при которой погрешность будет хотя бы примерно одинаковая. Хотя может с рандомом это не прокатит..
Умные люди подсказали что “честность” счетчика можно увеличить путем замены равенства на неравенство:
rand(1, 100) == 50
rand(1, 1000000) < 20000
Провел немного тестов, чем больше итераций проходим, тем меньше нужно делать правую часть неравенства.
Надо еще тестить =)
Я тесты на 100 итерация даже не делал. При таких малых величинах погрешности ого-го :) Я думаю стоит начинать минимум с 1к. Во всех остальных случаях проще использовать кэш или прямые запросы в БД
Просто одно дело, когда это счетчик посещений, другое когда счетчик просмотров новости/поста.
Кстати пришла в голову мысль как прикольнее использовать кэш для этих случаев. Обычно делают как: берут количество просмотров страницы, пишут в кэш, который потом инкриментируют. И раз в 10 минут, например, синхронизируют кэш с БД.
У меня идея такая: при первом просмотре страницы добавляем запись в кэш со значением “1″ (“page_$page_id”), все остальные просмотры будут эту запись инкриментировать. И каждые N инкриментов обновлять запись в БД. Тогда точность счетчика будет очень точной =) А максимум что мы можем потерять это те N просмотров, которые кэш вытеснит в случае нехватки памяти (но и это может случиться только тогда, когда новость совсем перестанут просматривать).
У данного способа есть и свои минусы. Если новость будет очень популярной, вы все равно создадите хорошую нагрузку на БД, так как будете обращаться к ней очень часто. Или количество итераций, при котором обновляем БД, должно быть очень большим. В итоге приходим к реализации дополнительной логики, которая будет следить за распределением нагрузки и автоматически увеличивать лимиты.
Для новостей с малой популярностью скорее всего данные кэша будут постоянно теряться. Если новость имеет, например, 30 просмотров в день, то данные будут выжиматься из кэша или БД придется обновлять каждые 1-2 просмотра, что лишает кэширование смысла.