Как построить кэш
Для начала предположим что наш абстрактный кэш в вакууме чуть-чуть конкретен – он является одним файлом.
Одновременный доступ
Первым делом определитесь с возможностью одновременного доступа.
Будут ли в кэш одновременно писать два процесса
Если да – запись становится блокирующей операцией. Один из процессов должен будет ждать, когда завершится предыдущий.
Будут ли кэш одновременно читать и писать два процесса
Здесь запись тоже станет блокирующей операцией. Но если гарантируется что писать будет только один процесс одновременно – можно сделать копию кэша, внести в неё изменения и под блокировкой заменить оригинал копией. В зависимости от того как работает замена под капотом – можно даже не блокировать.
Инвалидация и структура
Как мы будем структурировать кэш влияет на то, как будем инвалидировать.
Допустим, кэш содержит меняющийся набор записей ключ-значение, причем значение является коллекцией. Например словарь со значениями – списками строк.
Ключи могут добавляться и удаляться.
В значениях записи могут добавляться и удаляться. Удаление записей имеет особенность – значения могут через некоторое время появиться заново и при исчезновении далеко не факт что перестают быть актуальными. То есть их нужно удалять из кэша только если уверены, что они не появятся снова.
Есть что-то, обновляющее этот кэш периодически. Записей в кэше может быть много и при росте их числа нехорошо, если время их обработки будет расти линейно.
TTL
Решением является добавление TTL – времени жизни ключа и каждого элемента значения.
Одни записи не меняются почти никогда, другие меняются регулярно. Избежать постоянной обработки неизменяющихся записей и привести к более частой обработке часто изменяющихся может динамический TTL для ключа. Если запись меняется при обработке – уменьшаем TTL. Если значение остается прежним – наоборот увеличиваем. Не забудьте ввести ограничение для TTL снизу и сверху.
Необходимость обработки записи можно определять по формуле:
Текущая дата – Дата последнего обновления > TTL
Итоговая структура кэша
В итоге приходим к немного более сложной структуре:
cache = {
key: { last_update: …, ttl: …, value:
{
v1: {
last_update: …,
ttl: …
},
v2: …}
},
key2: {…}
}
Производительность
Дальнейшее сохранение работоспособности всей системы при росте числа можно достичь тремя способами.
Стараться делать быстрее
Можно просто профилировать и оптимизировать код обработки. Если кэш хранится в человекочитаемом виде (вот надо и всё тут во славу удобства отладки) и при обновлении может меняться несколько раз с сохранением – можно делать копию в быстрочитаемом машинном формате. Например для людей YAML/JSON (особенно с indent), для машины pickle. При замене копию сконвертировать обратно в человеческий формат.
Можно поменять железо, на котором это исполняется на более быстрое.
Стараться делать меньше
Тюним параметры TTL: минимум, максимум, прирост, уменьшение чтобы уменьшить число записей, которым нужна обработка.
Параллелить
Обработка при обновлении может проводиться несколькими тредами, когда не хватает одной машины, можно распределять задачи между несколькими машинами.
Можно партиционировать хэш тем или иным образом. При последовательной обработке снизится время в течение которого весь кэш непрерывно заблокирован, чтение воркером своего куска для обработки станет быстрее.