Комментарии 69
Dog pile effect эта ситуация называется.
+3
И вправду, код не очень читабелен.
Каким способом вы устраняете этот эффект? Не вижу, где инкремент тестируется на то, что не было выполнено одновременное увеличение двумя процессами.
Каким способом вы устраняете этот эффект? Не вижу, где инкремент тестируется на то, что не было выполнено одновременное увеличение двумя процессами.
+2
На всякий случай приведу рабочий код. Не факт, что оптимальный, но работает.
# Использование
v = DataCache.get_or_evaluate("key", 10.seconds) do
5 + 5 # evaluation block
end
# берет значение из кэша.
# возвращает его, если оно актуально, или если новое значение кем-то вычисляется.
# иначе устанавливает признак вычисления, вычисляет, пишет в кэш и возвращает.
def get_or_evaluate(key, expiry = 0)
return yield if @impl.nil?
key_in_eval = "#{key}:in_eval"
key_expiry_at = "#{key}:expiry_at"
@impl.add(key_in_eval, 0, 0, true) rescue nil
loop do
val = get(key)
in_eval = ((@impl.get(key_in_eval, true) || 0) rescue 0).to_i
expiry_at = ((@impl.get(key_expiry_at, true) || 0) rescue 0).to_i
logger.debug("In eval: #{in_eval}, Expiry at: #{expiry_at}, Now: #{Time.now.to_i}")
return val if val && (in_eval > 0 || expiry_at >= Time.now.to_i)
begin
break if @impl.incr(key_in_eval, 1) == 1
return val if @impl.decr(key_in_eval, 1) == 1
rescue
return val
end
sleep 0.1
end
delete(key_expiry_at)
begin
val = yield
set(key, val)
@impl.set(key_expiry_at, Time.now.to_i + expiry, 0, true)
ensure
@impl.decr(key_in_eval, 1)
end
val
end
0
Весь инкремент даден на откуп мемкешу. Если в 2-х процессах одновременно произойдёт инкремент — отлично. В нативном ключе ..._increment будет лежать значение, которое наинкрементил мемкеш. А в надстроенном массиве — просто бекап на случай, если кто-то захочет взять значение инкрементируемого ключа через обычный MC::get().
0
Разбил код на 2 части. Вторая часть немного больше, но там всего 6 небольших методов.
Самая большая рутина — в каждом методе распознавать инкрементные ключи и удалять нативный инкрементирующися ключ. значение
Самая большая рутина — в каждом методе распознавать инкрементные ключи и удалять нативный инкрементирующися ключ. значение
0
Подскажите, разъясните нубу, может я чего-то не понимаю, а чем плох (неправилен) такой подход, как ключ-backup для memcache — просто с увеличенным значением времени *жизни* ключа? Т.е. когда время жизни кончилось, отдаём ключ-backup (который старше на 5 секунд, скажем). Соответственно генерируем новые.
+1
Простите, но, возможно, вы иначе переизобрели CAS.
+1
Это избавит только от повторного set по ключу, а не от повторных вычислений сохраняемых данных.
+1
Не совсем так, но за пруфлинк спасибо. В моей реализации CAS заменит incr/decr.
0
Попробуйте в 10 потоков запустить следующий код на холодном кеше:
Как я заметил из исходников — 10 раз будет выполняться математика, пока кеш не нагреется. В случае релизов с полным обновлением кеша — это равносильно убитой тушке сервера.
$data = MC::get('some_key');
if ($data === FALSE)
{
sleep(60);
$data = "test string";
MC::set('some_key', $data, FALSE, 3600);
}
Как я заметил из исходников — 10 раз будет выполняться математика, пока кеш не нагреется. В случае релизов с полным обновлением кеша — это равносильно убитой тушке сервера.
0
За слив кеша у нас бъют по разным частям тела. И без этой логики это будет равносильно его ооочень скрипучей работе.
Локи — медленно и надо привязывать к этой надстройке, а не к голому мемкешу. Плюс — в каком виде хранить генераторы данных и организовывать очередь выполнения — тоже вопрос. Иначе сервера лягут, но уже от РНР :)
Локи — медленно и надо привязывать к этой надстройке, а не к голому мемкешу. Плюс — в каком виде хранить генераторы данных и организовывать очередь выполнения — тоже вопрос. Иначе сервера лягут, но уже от РНР :)
0
Сайт-то хоть живой? Можно в личку url? Интересно, что вы там такое монструозное обрабатываете на нескольких серверах.
0
Ну и в догонку — как вы обновляете кеш при изменении структуры хранения данных?
+1
а для тех кто с php не знаком можете плизз пояснить,
вот если 10 потоков запросили данные и их нет в кэше.
первый поток кто дорвался ставит флаг что вычисление идет,
остальные 9 видя этот флаг ждут типа sleep?
когда флаг пропадает, то опять запускается функция получения данных, так?
вот если 10 потоков запросили данные и их нет в кэше.
первый поток кто дорвался ставит флаг что вычисление идет,
остальные 9 видя этот флаг ждут типа sleep?
когда флаг пропадает, то опять запускается функция получения данных, так?
0
Сорри, ответил не туда: habrahabr.ru/blogs/php/128275/#comment_4249840
0
Это очень и очень приблизительный сценарий. В идеале код должен понять, что есть лок и показать пользователю что-то типа «обновление...» на месте, где должно быть значение из мемкеша.
В это время, первый дорвавшийся выставляет лок и обновляет данные. Либо ставит задачу в очередь на обновление и тоже умирает.
В это время, первый дорвавшийся выставляет лок и обновляет данные. Либо ставит задачу в очередь на обновление и тоже умирает.
0
Советую использовать Redis + команду WATCH.
+1
Это уже в ToDo листе. Redis или Mongo — нужен будет файловый кеш, чтобы при падении кеш-сервера/гибели мира/Чаке в датацентре и т.д. случаях кеш поднимался из файла сначала, а потом уже дёргал приложение.
0
Так Redis и как низколатентный кэш подходит. При наличии append-only режима и bgrewriteaof он вполне себе заменяет тот же персистентный memcacheDB. А ставите maxmemory-policy allkeys-lru — и у вас получается чистейшей воды memcached, только гораздо функциональнее.
0
habrahabr.ru/blogs/webdev/43540/
30 сентября 2008 года.
30 сентября 2008 года.
0
очень похожая тема goo.gl/dStVz
+1
а знаем ли мы про магию CAS?
0
Хм, а если перед генерацией данных ставить этот ключ в определенное значение например «in_progress» и проверять при выборке на это значение, если получили это значение значит зацикливаем проверку с шагом в определенное время перепроверяем? Хотя что-то не айс вариант чувствую :)
0
2 (10, 100, ...) процессов одновременно поставят in_progress и начнут генерировать данные одновременно.
0
Хм, ну тогда было бы здорово если мемкэш умел бы сам блочить для остальных процессов запись и чтение по этому ключу до тех пор пока процесс, обнаруживший протухшее значение, не закончит регенерацию данных :)
0
0
Еще из адекватных альтернатив вижу: не устанавливать expires время, а делать все данные бесконечно живущими, а при поступлении инфы, которая должна обновить данные, просто обновлять кэш, причем так, что бы если один процесс, который и принял инфу (он же ее и будет обновлять в кэше), не блокировал доступ к кэшу, а оставлял там старые данные чтоб остальные их могли получить беспрепятственно %) Вобщем нужно что-то вроде транзакций реализовать, тогда воркеры будут получать старые данные до тех пор пока один из них не обновит всю нужную инфу в кэше по требованию так сказать, причем обновить надо сразу и все что затронется, а не постепенно, целостность не нарушится так %)
0
А два процесса, одновременно обновляющие данные под одним ключом? :) Вроде механизм типа транзакций только внешними блокировками с memcache можно реализовать.
0
Охохо, ежели ничего не удалять — ужасающе быстро закончится место. В моём коде так и делается: один начинает генерацию, а остальные — «кто не успел :)» — получают старые данные.
0
Хотя, наверное, можно намутить что-то вроде
Но, имхо, во-первых, гарантий не даёт, что только один процесс будет обрабатывать ($key. '_in_progress' может удалиться, пока обработка ещё не завершена), во-вторых, не надежно (ситуация «ключ уже существует», «ключ не найден» и, например, «сервер не отвечает» Memcache не позволяет различить вроде как), в-третьих, если someHardFunction() зависнет, то все остальные будут ждать (это кодом можно решить, в принципе). Да и вообще как-то некрасиво :)
while(1) {
$res = $mc->add($key . '_in_progress', getmypid());
if ($res !== FALSE) {
$value = someHardFunction(); // TODO: DI
$mc->set($key, $value);
$mc->delete($key . '_in_progress');
return $value;
}
while($mc->get($key . '_in_progress') !== FALSE) {
usleep(100);
}
$value = $mc->get($key);
if ($value !== FALSE) {
return $value;
}
}
Но, имхо, во-первых, гарантий не даёт, что только один процесс будет обрабатывать ($key. '_in_progress' может удалиться, пока обработка ещё не завершена), во-вторых, не надежно (ситуация «ключ уже существует», «ключ не найден» и, например, «сервер не отвечает» Memcache не позволяет различить вроде как), в-третьих, если someHardFunction() зависнет, то все остальные будут ждать (это кодом можно решить, в принципе). Да и вообще как-то некрасиво :)
0
Ну тогда не ожидать удаления $key. '_in_progress', если не получилось его добавить, а возвращать ошибку. Для веба 409-ю (прежде всего для POST, PUT, DELETE) с просьбой повторить запрос позже. Или (для GET-запросов) возвращать текущее состояние кэша со статусом 203.
Или на асинхронное программирование переходить — вроде для него это типичная задача :)
Или на асинхронное программирование переходить — вроде для него это типичная задача :)
0
> 2 (10, 100, ...) процессов одновременно поставят in_progress и начнут генерировать данные одновременно.
не поставят.
какой первый поток доберется, тот и успеет поставить флаг.
остальные его не получат и просто будут в sleep и проверять каждые N мс,
как только данные сгенерятся, то флаг первый поток снимет, и остальные тут же получат все данные.
не поставят.
какой первый поток доберется, тот и успеет поставить флаг.
остальные его не получат и просто будут в sleep и проверять каждые N мс,
как только данные сгенерятся, то флаг первый поток снимет, и остальные тут же получат все данные.
0
В memcache cas нет, он только в memcached
0
тут можно без cas, через add.
code.google.com/p/memcached/wiki/FAQ#Emulating_locking_with_the_add_command
code.google.com/p/memcached/wiki/FAQ#Emulating_locking_with_the_add_command
0
и с шагом*
0
Совсем не хочется иметь N висящих потоков. Не к добру это.
+1
Да, но они хотя бы могут сидеть в sleep не нагружая ни процессоры ни базу.
Альтернатива — отдать ошибку, но лучше ли это?
Альтернатива — отдать ошибку, но лучше ли это?
0
Они будут жрать оперативу. Так что для высоких нагрузок не очень вариант…
0
Если конфликт скорее исключение, чем правило, да ещё клиент не тупой браузер, а свой (JS-приложение в тупом браузере :), например), который через некоторое время запрос повторит без вмешательства пользователя, то лучше, имхо, ошибку отдать.
0
300 МБ исходников, часть которых завязана на мемкеш и логику работы с ним. Не получится без конфликтов никак — это нормальное поведение. Запрос — нет данных — генерация — в промежутке ещё 100 запросов. Для того и делалось :)
0
А собственно как поведёт себя приложение, когда сразу после старта получит 100 одинаковых запросов? Первый начнёт генерацию, а остальные 99 что вернут? Данных ещё нет же.
P.S.
не слишком избыточно?
не достаточно будет? Или это отточенная на реальных данных оптимизация?
P.S.
return (is_array($value) &&
isset($value['_dc_life_end']) && isset($value['_dc_cache_time']) &&
!empty($value['_dc_life_end']) && !empty($value['_dc_cache_time'])
) ? TRUE : FALSE;
не слишком избыточно?
return !empty($value['_dc_life_end']) && !empty($value['_dc_cache_time'];
не достаточно будет? Или это отточенная на реальных данных оптимизация?
0
1. Сценарий запуска пока кеш не разогреется — апокалиптичен по определению. У нас бъют сильно-сильно по всяким выступающим частям тела за слив мемкеша (либо админа за рестарт и т.п. — за что что недоглядел).
2. Максимальная оптимизация, которую тут можно провести:
Ибо должна произвестись ошибка уровня E_NOTICE при использовании одних только empty(). Даже если они по-умолчанию отключены — не значит что их нет :) Да и isset() и empty() — функции, предназначенные совершенно для разных вещей и какбе логично сначала проверить существование элемента массива (если нет — ветка просто обломается и дальше считаться не будет), а потом уже — его пустоту.
2. Максимальная оптимизация, которую тут можно провести:
return (is_array($value) &&
isset($value['_dc_life_end']) && isset($value['_dc_cache_time']) &&
!empty($value['_dc_life_end']) && !empty($value['_dc_cache_time'])
);
Ибо должна произвестись ошибка уровня E_NOTICE при использовании одних только empty(). Даже если они по-умолчанию отключены — не значит что их нет :) Да и isset() и empty() — функции, предназначенные совершенно для разных вещей и какбе логично сначала проверить существование элемента массива (если нет — ветка просто обломается и дальше считаться не будет), а потом уже — его пустоту.
0
Упс, забыл, что нотайсы по дефолту выключены, когда тестил — недавно систему переставлял, php.ini ещё не трогал. Без is_array() нотайсы даст и isset()?
0
Даст, но не во всех случаях.
1. НЕ даст при обращении к строке как к массиву, если индекс меньше длины строки или индекс не цифровой. Изучите вывод, но не забудьте включить E_NOTICE.
2. НЕ даст при обращении к объекту, который реализует ArrayAccess интерфейс
3. НЕ даст при обращении к числу (в т.ч. и флоату) как к массиву
4. НЕ даст при обращении к булевой переменной как к массиву. (Пример поменять самому);
5. ДАСТ ошибку уровня E_USER_ERROR (Fatal в простонародье) обращение к объекту, который не реализует интерфейс ArrayAccess. В пример добавить
Фууух
1. НЕ даст при обращении к строке как к массиву, если индекс меньше длины строки или индекс не цифровой. Изучите вывод, но не забудьте включить E_NOTICE.
error_reporting(2147483647);
$a = 'qwerty';
var_dump(isset($a[2]), $a[2], isset($a[9]), $a[9], isset($a['some_key']), $a['some_key']);
2. НЕ даст при обращении к объекту, который реализует ArrayAccess интерфейс
3. НЕ даст при обращении к числу (в т.ч. и флоату) как к массиву
error_reporting(2147483647);
$a = 5.7;
var_dump(isset($a[2]), $a[2], isset($a[9]), $a[9], isset($a['some_key']), $a['some_key']);
4. НЕ даст при обращении к булевой переменной как к массиву. (Пример поменять самому);
5. ДАСТ ошибку уровня E_USER_ERROR (Fatal в простонародье) обращение к объекту, который не реализует интерфейс ArrayAccess. В пример добавить
$a = new stdClass;
Фууух
0
6. ДАСТ такую же ошибку как в п. 5 при касте массива в объект
$a = array('a' => 111);
$a = (object)$a;
var_dump(isset($a[0]), $a[0], isset($a[9]), $a[9], isset($a['some_key']), $a['some_key']);
0
> В документации сказано, что наибольшее время хранения ключа — 30 дней.
Это не совсем верно. Можно заставить данные жить и дольше, если передавать не время жизни, а unix timestamp момента, когда они должны протухнуть.
Это не совсем верно. Можно заставить данные жить и дольше, если передавать не время жизни, а unix timestamp момента, когда они должны протухнуть.
+1
Смирнов еще в 2008 изложил отличный подход к этому велосипеду. "Кэширование и memcached".
0
Зарегистрируйтесь на Хабре, чтобы оставить комментарий
Lock-free memcache API