
Всем привет. Да, знаю, я давно не писал. Ну простите, и как это не банально, был занят. А заставила меня написать одна мысля. И пожалуйста, дочитайте это до конца, потому что или это очень круто или я опять что то не понимаю, и с температурой 38 мне лучше за клаву не садиться.
Кеш. Я им пользуюсь для того, чтобы данные, которые я долго вычисляю — положить в память куда нить, чтоб если они понадобились — быстро их оттуда взять. Ну а если их там нет, то просто пересчитать и положить. Если вы им пользуетесь также, то читайте дальше иначе напишите комментарий, который начнется со слов: «Тю, блин, а я его совершенно по другому юзаю, глянь…»
Т.е. на сетах и гетах все сводится к примерно следующему алгоритму.
-
from django.core.cache import cache
-
-
def setter(key,l_value,timeout=0):
-
val = cache.get(key)
-
if val is None:
-
val = l_value()
-
cache.set(key,val,timeout)
-
return val
где l_value — это ссылка на функцию, значение которой будет получено, в случае если его нет в кеше.
Вот этот умопомрачительный алгоритм у меня лежит в основе кеширования.
Хух… если у вас также, то идете дальше. Надеюсь сейчас осталось достаточно народу.
Прогуливаясь легкой и непринужденной походкой по блогосфере рунете я уже в который раз натыкаюсь на довольно странное решение следующей проблемы.
Если есть какие либо данные которые системе нужны часто, но вычисляются долго, то их прямое получение по алгоритму, описанному выше — просто убивает систему. Потому что как только они пропадают из кеша — все, кому нужны эти данные — начинают скопом — все вместе их получать. Например статистика по пользователям у вас вычисляется 5 сек, а выводится на главной странице, с посещаемостью 50 чел в сек, значит одновременно эти данные будут получать 250 процессов – что, может привести к смерти.
Решение рунета — 2 кеша. В один кладем с одним эксперейшеном, в другой с таким же, но чуть больше. Я думаю многие натыкались на такие решения, но вкратце — если заэкспаирилось в первом — берем из второго, но первый, кто узнал, о том, что заэкспаирилось — пересчитывает.
Тут просто тьма тьмущая узких мест
1.Старт у системы должен быть особый. Т.е. в нулевой точке в кеше уже должны быть часто доступные данные.
2.У вас двойные данные в кеше, т.е. две копии, а ведь часто бывает и такое, что трудновычисляемые данные — это и большие данные.
3.И последнее — если процесс, который вычисляет заекспаревшиеся данные — умирает. То умирают все. Явно теряем в отказоустойчивости.
Кратко опишу свой алгоритм решения, и построенный на нем джанговый кешовый бекенд (за базовый взят мемкешовый).
Если в ячейку с ключем класть не данные, а хеш из двух значений — данные, и время, когда их надо обновить. (ТАДАМ избавились от второго пункта)
А что если ты перед началом вычислений будеш класть в другой системый и уникальный ключ в кеше время, когда первый, начавший вычисления – планирует их закончить. А остольные процессы, которые захотят получить данные и не увидят их — смогут орентироваться на системный ключь, чтоб понимать, что данные скоро будут и их необходимо подождать или мы не дождались и попробуем еще раз. (ТАДАМ избавились от первого и третьего)
А теперь скучный код. Чтоб легче было читать — его необходимо скрестить с алгоритмом, который я описывал выше. И если функция гет — вернет None то эти данные сразу начнут вычисляться.
Небольшой рандом необходим, чтоб все процессы сразу не набросились вычислять после первого сдавшегося, а нарастающий таймаут необходим для быстрого избавления от быстрых данные и размеренного ожидания долгих.
-
from django.core.cache.backends.memcached import CacheClass as BaseCacheClass
-
from datetime import datetime,timedelta
-
from time import sleep
-
from random import random
-
from time import sleep
-
-
ADDITION_EXP_TIME = 20
-
TIME_FOR_CREATE = 5
-
-
class CacheClass(BaseCacheClass):
-
def add(self, key, value, timeout=0):
-
timeout = timeout or self.default_timeout
-
value = {'v':value,'e':datetime.now()+timedelta(seconds=timeout)}
-
super(CacheClass,self).add(key,value,timeout+ADDITION_EXP_TIME)
-
-
def set(self, key, value, timeout=0):
-
timeout = timeout or self.default_timeout
-
value = {'v':value,'e':datetime.now()+timedelta(seconds=timeout)}
-
super(CacheClass,self).set(key,value,timeout+ADDITION_EXP_TIME)
-
-
def get(self,key, default=None):
-
wait_next_val = 0
-
while True:
-
wait_next_val += 0.1
-
value = super(CacheClass,self).get(key,default)
-
now = datetime.now()
-
-
if value is not None and now<value['e']:
-
return value['v']
-
-
wait_system_key = 'wait_system__%s__wait_system'%key
-
wait_system = super(CacheClass,self).get(wait_system_key)
-
-
# if you find expired key first or you don't wait the next person
-
if wait_system is None or wait_system<now:
-
super(CacheClass,self).set(wait_system_key,datetime.now()+timedelta(seconds=TIME_FOR_CREATE),TIME_FOR_CREATE + 5)
-
return None
-
-
#if somebody already getting a new value
-
if value is not None:
-
return value['v']
-
-
sleep(random()*wait_next_val)
И на всякий случай. Если вы все таки считаете это отличной идее. Кладем это в файлик с незамысловатым названием smart_cache.py рядом с settings.py, а в settings.py записываем
-
CACHE_BACKEND = "smart_cache://127.0.0.1:11211"