Недействительность кеша - есть ли общее решение?


118

«В информатике есть только две сложные проблемы: недействительность кеша и присвоение имен вещам».

Фил Карлтон

Есть ли общее решение или способ сделать кеш недействительным; чтобы знать, когда запись устарела, чтобы всегда получать свежие данные?

Например, рассмотрим функцию, getData()которая получает данные из файла. Он кэширует его на основе времени последнего изменения файла, которое он проверяет каждый раз при вызове.
Затем вы добавляете вторую функцию, transformData()которая преобразует данные и кэширует ее результат для следующего вызова функции. Он не знает файла - как добавить зависимость, согласно которой при изменении файла этот кеш становится недействительным?

Вы можете вызывать getData()каждый раз, когда transformData()вызывается, и сравнивать его со значением, которое использовалось для создания кеша, но это может оказаться очень дорогостоящим.


6
Я считаю, что он имеет какое-то отношение к написанию X Windows
Грег

1
Я думаю, что заголовок был бы лучше, как «Недействительность кеша - есть ли общее решение?» поскольку это относится к определенному классу проблемы кеширования.
RBarryYoung

71
Нет, он плохо разбирался в информатике. Я уверен, что его участие в создании OpenGL, X11 и SSLv3 сделало его слишком занятым, чтобы серьезно его изучать. :-)
Тим Лешер

80
В информатике есть только две сложные проблемы: инвалидация кеша. Именование вещей. И погрешности поэтапные.
The Dag

8
Однажды я слышал это как"The two hardest things in Computer Science are cache invalidation, naming things, and off-by-one errors."
Джонатон Рейнхарт

Ответы:


55

То, о чем вы говорите, - это цепочка зависимостей на протяжении всей жизни, когда одна вещь зависит от другой, которая может быть изменена вне ее контроля.

Если у вас есть функция идемпотентную от a, bк cкоторой, если aи bте же , то cодно и то же , но стоимость проверки bвысока , то вы либо:

  1. согласны с тем, что иногда вы работаете с устаревшей информацией и не всегда проверяете b
  2. сделай все возможное, чтобы сделать проверку bкак можно быстрее

Вы не можете съесть свой пирог ...

Если вы можете наложить дополнительный кеш на aверхний слой, то это никак не повлияет на исходную проблему. Если вы выбрали 1, у вас есть свобода, которую вы предоставили себе, и, таким образом, вы можете кэшировать больше, но должны помнить о допустимости кешированного значения b. Если вы выбрали 2, вы все равно должны проверять bкаждый раз, но можете вернуться к кешу, aесли bпроверяет.

Если вы накладываете кеши на слои, вы должны учитывать, не нарушили ли вы «правила» системы в результате комбинированного поведения.

Если вы знаете, что он aвсегда действителен, если bон есть, вы можете организовать свой кеш следующим образом (псевдокод):

private map<b,map<a,c>> cache // 
private func realFunction    // (a,b) -> c

get(a, b) 
{
    c result;
    map<a,c> endCache;
    if (cache[b] expired or not present)
    {
        remove all b -> * entries in cache;   
        endCache = new map<a,c>();      
        add to cache b -> endCache;
    }
    else
    {
        endCache = cache[b];     
    }
    if (endCache[a] not present)     // important line
    {
        result = realFunction(a,b); 
        endCache[a] = result;
    }
    else   
    {
        result = endCache[a];
    }
    return result;
}

Очевидно , что последовательное наслоение (скажем x) тривиально, пока на каждой стадии действительность вновь добавленного ввода отвечает a: bсоотношение для x: bа x: a.

Однако вполне возможно, что вы могли бы получить три входа, действительность которых была полностью независимой (или была циклической), поэтому разделение на слои было бы невозможно. Это будет означать, что строку с пометкой // важно изменить на

if (endCache [a] истек или отсутствует)


3
или, может быть, если стоимость проверки b высока, вы используете pubsub, чтобы при изменении b он уведомлял c. Паттерн Наблюдатель распространен.
user1031420

15

Проблема с недействительностью кеша заключается в том, что данные меняются без нашего ведома. Таким образом, в некоторых случаях решение возможно, если есть что-то еще, что знает об этом и может уведомить нас. В данном примере функция getData может подключиться к файловой системе, которая знает обо всех изменениях в файлах, независимо от того, какой процесс изменяет файл, и этот компонент, в свою очередь, может уведомить компонент, преобразующий данные.

Я не думаю, что есть какое-то общее волшебное решение, чтобы решить эту проблему. Но во многих практических случаях вполне могут быть возможности трансформировать подход, основанный на «опросе», в подход, основанный на «прерывании», что может просто решить проблему.


3

Если вы собираетесь использовать getData () каждый раз, когда выполняете преобразование, значит, вы полностью лишили кеша.

В вашем примере кажется, что решение будет заключаться в том, что при генерации преобразованных данных также сохраняется имя файла и время последнего изменения файла, из которого были сгенерированы данные (вы уже сохранили это в любой структуре данных, возвращенной getData ( ), поэтому вы просто копируете эту запись в структуру данных, возвращаемую transformData ()), а затем, когда вы снова вызываете transformData (), проверяйте время последнего изменения файла.


3

IMHO, функциональное реактивное программирование (FRP) в некотором смысле является общим способом решения проблемы недействительности кеша.

Вот почему: устаревшие данные в терминологии FRP называются сбоями . Одна из целей FRP - гарантировать отсутствие сбоев.

FRP более подробно объясняется в этом выступлении «Суть FRP» и в этом SO-ответе .

В разговоре , что Cells представляют собой кэшированные Object / Entity и Cellобновляется , если один из его зависимости обновляется.

FRP скрывает соединительный код, связанный с графом зависимостей, и обеспечивает отсутствие устаревших Cells.


Другой способ (отличный от 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 они также стремятся решить проблему недействительности. Это, конечно, ограничивает операции, которые связывают изменяемые данные с производными данными.


2

Сейчас я работаю над подходом, основанным на PostSharp и функциях мемоизации . Я пропустил его мимо своего наставника, и он согласен с тем, что это хорошая реализация кеширования без учета содержимого.

Каждую функцию можно пометить атрибутом, указывающим срок ее действия. Каждая отмеченная таким образом функция запоминается, а результат сохраняется в кеше с хешем вызова функции и параметрами, используемыми в качестве ключа. Я использую Velocity для бэкэнда, который обрабатывает распределение данных кеша.


1

Есть ли общее решение или метод создания кеша, чтобы знать, когда запись устарела, чтобы вы всегда получали свежие данные?

Нет, потому что все данные разные. Некоторые данные могут быть «устаревшими» через минуту, некоторые через час, а некоторые могут сохраняться в течение нескольких дней или месяцев.

Что касается вашего конкретного примера, самым простым решением является наличие функции «проверки кеша» для файлов, которую вы вызываете как из, так getDataи из transformData.


1

Нет общего решения, но:

  • Кэш может выступать в роли прокси (вытягивать). Предположим, что ваш кеш знает временную метку последнего изменения источника, когда кто-то звонит getData(), кеш запрашивает у источника временную метку последнего изменения, если то же самое, он возвращает кеш, в противном случае он обновляет свое содержимое исходным и возвращает его содержимое. (Вариант - клиент напрямую отправляет метку времени по запросу, источник будет возвращать контент только в том случае, если его метка времени отличается.)

  • Вы по-прежнему можете использовать процесс уведомления (push), кеш наблюдает за источником, если источник изменяется, он отправляет уведомление в кеш, который затем помечается как «грязный». Если кто-то вызывает getData()кеш сначала обновляется до источника, снимите флаг «грязный»; затем верните его содержимое.

Выбор, вообще говоря, зависит от:

  • Частота: многие вызовы getData()предпочли бы push, чтобы избежать заливки источника функцией getTimestamp
  • Ваш доступ к источнику: владеете ли вы исходной моделью? Если нет, скорее всего, вы не можете добавить процесс уведомления.

Примечание. Поскольку использование временной метки является традиционным способом работы http-прокси, другой подход - это совместное использование хэша сохраненного контента. Единственный известный мне способ обновления двух сущностей - это либо я позвоню вам (тянуть), либо вы позвоните мне… (толкнуть) и все.


0

кеш сложен, потому что вам нужно учитывать: 1) кеш - это несколько узлов, для них нужен консенсус 2) время недействительности 3) состояние гонки, когда происходит несколько операций получения / установки

это хорошее чтение: https://www.confluent.io/blog/turning-the-database-inside-out-with-apache-samza/


-2

Возможно, алгоритмы без учета кеша будут наиболее общими (или, по крайней мере, менее зависимыми от конфигурации оборудования), поскольку они сначала будут использовать самый быстрый кеш и двигаться дальше. Вот лекция Массачусетского технологического института: алгоритмы забывающего кеширования


3
Я думаю, что он не говорит об аппаратных кэшах - он говорит о своем коде getData (), имеющем функцию, которая «кэширует» данные, полученные из файла, в память.
Alex319
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.