«В информатике есть только две сложные проблемы: недействительность кеша и присвоение имен вещам».
Фил Карлтон
Есть ли общее решение или способ сделать кеш недействительным; чтобы знать, когда запись устарела, чтобы всегда получать свежие данные?
Например, рассмотрим функцию, getData()
которая получает данные из файла. Он кэширует его на основе времени последнего изменения файла, которое он проверяет каждый раз при вызове.
Затем вы добавляете вторую функцию, transformData()
которая преобразует данные и кэширует ее результат для следующего вызова функции. Он не знает файла - как добавить зависимость, согласно которой при изменении файла этот кеш становится недействительным?
Вы можете вызывать getData()
каждый раз, когда transformData()
вызывается, и сравнивать его со значением, которое использовалось для создания кеша, но это может оказаться очень дорогостоящим.
"The two hardest things in Computer Science are cache invalidation, naming things, and off-by-one errors."
Ответы:
То, о чем вы говорите, - это цепочка зависимостей на протяжении всей жизни, когда одна вещь зависит от другой, которая может быть изменена вне ее контроля.
Если у вас есть функция идемпотентную от
a
,b
кc
которой, еслиa
иb
те же , тоc
одно и то же , но стоимость проверкиb
высока , то вы либо:b
b
как можно быстрееВы не можете съесть свой пирог ...
Если вы можете наложить дополнительный кеш на
a
верхний слой, то это никак не повлияет на исходную проблему. Если вы выбрали 1, у вас есть свобода, которую вы предоставили себе, и, таким образом, вы можете кэшировать больше, но должны помнить о допустимости кешированного значенияb
. Если вы выбрали 2, вы все равно должны проверятьb
каждый раз, но можете вернуться к кешу,a
еслиb
проверяет.Если вы накладываете кеши на слои, вы должны учитывать, не нарушили ли вы «правила» системы в результате комбинированного поведения.
Если вы знаете, что он
a
всегда действителен, еслиb
он есть, вы можете организовать свой кеш следующим образом (псевдокод):Очевидно , что последовательное наслоение (скажем
x
) тривиально, пока на каждой стадии действительность вновь добавленного ввода отвечаетa
:b
соотношение дляx
:b
аx
:a
.Однако вполне возможно, что вы могли бы получить три входа, действительность которых была полностью независимой (или была циклической), поэтому разделение на слои было бы невозможно. Это будет означать, что строку с пометкой // важно изменить на
источник
Проблема с недействительностью кеша заключается в том, что данные меняются без нашего ведома. Таким образом, в некоторых случаях решение возможно, если есть что-то еще, что знает об этом и может уведомить нас. В данном примере функция getData может подключиться к файловой системе, которая знает обо всех изменениях в файлах, независимо от того, какой процесс изменяет файл, и этот компонент, в свою очередь, может уведомить компонент, преобразующий данные.
Я не думаю, что есть какое-то общее волшебное решение, чтобы решить эту проблему. Но во многих практических случаях вполне могут быть возможности трансформировать подход, основанный на «опросе», в подход, основанный на «прерывании», что может просто решить проблему.
источник
Если вы собираетесь использовать getData () каждый раз, когда выполняете преобразование, значит, вы полностью лишили кеша.
В вашем примере кажется, что решение будет заключаться в том, что при генерации преобразованных данных также сохраняется имя файла и время последнего изменения файла, из которого были сгенерированы данные (вы уже сохранили это в любой структуре данных, возвращенной getData ( ), поэтому вы просто копируете эту запись в структуру данных, возвращаемую transformData ()), а затем, когда вы снова вызываете transformData (), проверяйте время последнего изменения файла.
источник
IMHO, функциональное реактивное программирование (FRP) в некотором смысле является общим способом решения проблемы недействительности кеша.
Вот почему: устаревшие данные в терминологии FRP называются сбоями . Одна из целей FRP - гарантировать отсутствие сбоев.
FRP более подробно объясняется в этом выступлении «Суть FRP» и в этом SO-ответе .
В разговоре , что
Cell
s представляют собой кэшированные Object / Entity иCell
обновляется , если один из его зависимости обновляется.FRP скрывает соединительный код, связанный с графом зависимостей, и обеспечивает отсутствие устаревших
Cell
s.Другой способ (отличный от FRP), который я могу придумать, - это обернуть вычисленное значение (типа
b
) в некую монаду записи,Writer (Set (uuid)) b
гдеSet (uuid)
(нотация Haskell) содержит все идентификаторы изменяемых значений, от которыхb
зависит вычисленное значение . Итак,uuid
это своего рода уникальный идентификатор, который определяет изменяемое значение / переменную (скажем, строку в базе данных), от которойb
зависит вычисленное значение .Объедините эту идею с комбинаторами, которые работают с этой монадой записи, и это может привести к какому-то общему решению недействительности кеша, если вы используете эти комбинаторы только для вычисления нового
b
. Такие комбинаторы (скажем, специальная версияfilter
) принимают монады Writer и(uuid, a)
-s в качестве входных данных, гдеa
- изменяемые данные / переменная, идентифицируемая с помощьюuuid
.Таким образом, каждый раз, когда вы изменяете «исходные» данные
(uuid, a)
(скажем, нормализованные данные в базе данных, из которойb
были вычислены), от которыхb
зависит вычисленное значение типа, вы можете аннулировать кеш, который содержит,b
если вы измените любое значение,a
от которогоb
зависит вычисленное значение. , потому что на основеSet (uuid)
в Writer Monad вы можете сказать, когда это произойдет.Таким образом, каждый раз, когда вы изменяете что-то с заданным
uuid
, вы транслируете эту мутацию всем кешам, и они аннулируют значения,b
которые зависят от изменяемого значения, идентифицированного с помощью указанного,uuid
потому что монада Writer, в которуюb
завернут, может сказать,b
зависит ли это от указанногоuuid
или не.Конечно, это окупается, только если вы читаете гораздо чаще, чем пишете.
Третий, практический подход - использовать материализованные представления в базах данных и использовать их в качестве кэшей. AFAIK они также стремятся решить проблему недействительности. Это, конечно, ограничивает операции, которые связывают изменяемые данные с производными данными.
источник
Сейчас я работаю над подходом, основанным на PostSharp и функциях мемоизации . Я пропустил его мимо своего наставника, и он согласен с тем, что это хорошая реализация кеширования без учета содержимого.
Каждую функцию можно пометить атрибутом, указывающим срок ее действия. Каждая отмеченная таким образом функция запоминается, а результат сохраняется в кеше с хешем вызова функции и параметрами, используемыми в качестве ключа. Я использую Velocity для бэкэнда, который обрабатывает распределение данных кеша.
источник
Нет, потому что все данные разные. Некоторые данные могут быть «устаревшими» через минуту, некоторые через час, а некоторые могут сохраняться в течение нескольких дней или месяцев.
Что касается вашего конкретного примера, самым простым решением является наличие функции «проверки кеша» для файлов, которую вы вызываете как из, так
getData
и изtransformData
.источник
Нет общего решения, но:
Кэш может выступать в роли прокси (вытягивать). Предположим, что ваш кеш знает временную метку последнего изменения источника, когда кто-то звонит
getData()
, кеш запрашивает у источника временную метку последнего изменения, если то же самое, он возвращает кеш, в противном случае он обновляет свое содержимое исходным и возвращает его содержимое. (Вариант - клиент напрямую отправляет метку времени по запросу, источник будет возвращать контент только в том случае, если его метка времени отличается.)Вы по-прежнему можете использовать процесс уведомления (push), кеш наблюдает за источником, если источник изменяется, он отправляет уведомление в кеш, который затем помечается как «грязный». Если кто-то вызывает
getData()
кеш сначала обновляется до источника, снимите флаг «грязный»; затем верните его содержимое.Выбор, вообще говоря, зависит от:
getData()
предпочли бы push, чтобы избежать заливки источника функцией getTimestampПримечание. Поскольку использование временной метки является традиционным способом работы http-прокси, другой подход - это совместное использование хэша сохраненного контента. Единственный известный мне способ обновления двух сущностей - это либо я позвоню вам (тянуть), либо вы позвоните мне… (толкнуть) и все.
источник
кеш сложен, потому что вам нужно учитывать: 1) кеш - это несколько узлов, для них нужен консенсус 2) время недействительности 3) состояние гонки, когда происходит несколько операций получения / установки
это хорошее чтение: https://www.confluent.io/blog/turning-the-database-inside-out-with-apache-samza/
источник
Возможно, алгоритмы без учета кеша будут наиболее общими (или, по крайней мере, менее зависимыми от конфигурации оборудования), поскольку они сначала будут использовать самый быстрый кеш и двигаться дальше. Вот лекция Массачусетского технологического института: алгоритмы забывающего кеширования
источник