Идея решения для инкрементных обновлений с использованием кеша браузера

Некоторое время назад я спросил, как сделать Дополнительные обновления с использованием кеша браузера. Здесь я даю краткое описание проблемы - для получения более подробного контекста, особенно причины, по которой я хочу это сделать, обратитесь к старому вопросу. Я бы хотел, чтобы вы обзор и улучшение мою идею решения (просто идею, так что не отправляйте меня обзор кода: D).

Проблема

Клиент (одностраничное приложение) получает с сервера довольно большие списки. Это отлично работает и фактически экономит ресурсы сервера как

  • один и тот же список может быть подан нескольким клиентам
  • а клиенты выполняют фильтрацию и сортировку, не беспокоя сервер снова и снова.

Некоторые из этих списков относятся к конкретному пользователю, другие - к группе пользователей, другие - к глобальному. Все эти списки могут измениться в любое время, и мы никогда не хотим обслуживать устаревшие данные (HTTP-заголовки Cache-Control и Expires здесь не используются).

Мы используем 304 NOT MODIFIED, который помогает в случае изменения ничего такого. Когда что-то меняется, изменения обычно небольшие, но HTTP вообще не поддерживает этот случай, поэтому мы должны отправить весь список, включая неизмененные части. Вместо этого мы можем отправить дельту, но нет очевидного способа, как это может быть эффективно кэшировано браузером (кеширование в localStorage или аналогичном далеко не так хорошо, как я объяснил в моем связанном вопросе).

Важным свойством наших списков является то, что каждый элемент имеет уникальный id и последний измененный timestamp. timestamp позволяет нам легко вычислить дельту, найдя элементы, которые недавно изменились. id позволяет нам применять дельту, просто заменяя соответствующие элементы (список внутренне является Map<Id, Item>). Это не сработает для удалений, но пока не будем их игнорировать.

Идея

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

  • WEEK Это базовый список, содержащий элементы все, поскольку они существовали в произвольный момент времени в текущая неделя.

  • DAY Список, содержащий все элементы с изменилось на этой неделе, кроме сегодняшнего дня, поскольку они существовали в произвольный момент времени в сегодня. Элементы, измененные сегодня, могут быть включены или не включены.

  • CURRENT Список, содержащий все элементы, которые изменили Cегодня, поскольку они существуют прямо сейчас.

Клиент получает все три списка. Он начинается с WEEK, применяет DAY (т.е. вставляет новые элементы и заменяет старые) и, наконец, применяет CURRENT.

Пример

Предположим, что в списке 1000 элементов, из которых 10 элементов меняются в день.

Список WEEK содержит все 1000 элементов, но его можно кэшировать до конца недели. Его точное содержимое не указано, и у разных клиентов могут быть разные его версии (при условии, что выполняется условие из вышеприведенного маркера). Это позволяет серверу кэшировать данные в течение целой недели, но также позволяет отбрасывать их, поскольку обслуживание текущего состояния тоже нормально.

Список DAY содержит до 70 элементов и может храниться в кэше до конца дня.

Список CURRENT содержит до 10 элементов и может быть кэширован только до тех пор, пока что-либо не изменится.

Связи

Клиент ничего не должен знать об используемой шкале времени, но ему нужно знать количество списков, которые нужно запросить. "Классический" запрос вроде

GET /api/order/123      // get the whole list with up to date content

будут заменены тремя запросами вроде

GET /api/0,order/123    // get the WEEK list
GET /api/1,order/123    // get the DAY list
GET /api/2,order/123    // get the CURRENT list

Вопросы

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

Вы видите другие проблемы с этой идеей?

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

Есть улучшения?

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
5
0
139
2

Ответы 2

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

All these lists may change anytime and we never want to serve stale data

Таким образом, вы должны использовать большое время кеширования и URL-адреса для очистки кеша.

We're using 304 NOT MODIFIED

Это наихудший способ решить проблему. Большая часть затрат на извлечение связана с задержкой. Если вы отвечаете с ответом 304, значит, вы уже понесли большую часть затрат - это будет особенно заметно, когда вы имеете дело с небольшими фрагментами данных. HTTP / 2 помогает (по сравнению с 1.0 и 1.1), но не снижает затрат.

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

Что касается влияния на производительность: при запуске клиент получает несколько списков одновременно, и это обычно означает некоторую задержку (в настоящее время в среднем менее одной секунды; в любом случае, наши пользователи не слишком заботятся о начальной задержке, поскольку они не случайные люди вводят нас в заблуждение случайными ссылками в Интернете). +++"произносится, когда вы имеете дело с небольшими фрагментами данных" - Поэтому мы используем довольно большие списки. После некоторой начальной задержки у клиента есть все необходимое.

maaartinus 13.05.2018 05:36

К сожалению, я действительно не понимаю, как использовать URL-адреса блокировки кеша. Переключение с style.v1.css на style.v2.css можно жестко запрограммировать в клиенте, поскольку изменение содержимого style.css происходит при обновлении клиента. Изменение содержимого /api/orders может произойти в любое время, и клиент просто не может знать. Даже если бы мы предоставили /api/orders?version=123456, клиент все равно будет спрашивать сервер, когда версия для загрузки.

maaartinus 13.05.2018 05:38

Я предполагаю, что вы понимаете следующие общие проблемы своего подхода:

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

Исходя из этого, мне нравится ваш подход.

Я могу изменить кое-что. Это добавляет немного гибкости, но также немного усложняет. Это может быть, а может и не быть хорошей идеей.

Вместо жесткого кодирования трех URL-адресов на клиенте вы можете отправлять явные гиперссылки в заголовках ответов. Вот как это может работать:

Клиент запрашивает жестко запрограммированную «точку входа»:

> GET /api/order/123?delta=all

< 200 OK
< Cache-Control: max-age=604800
< Delta-Location: /api/order/123?delta=604800
<
< [...your WEEK list...]

Увидев заголовок Delta-Location, клиент затем запрашивает его и применяет полученную дельту:

> GET /api/order/123?delta=604800

< 200 OK
< Cache-Control: max-age=86400
< Delta-Location: /api/order/123?delta=86400
<
< [...your DAY list...]

И так до тех пор, пока в ответе нет Delta-Location.

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

В частности, это позволяет решить проблему всплесков. После выполнения изменения массы вы можете начать обслуживать гораздо меньшие дельты (с соответственно меньшим max-age), так что они исключают изменение массы. Затем вы постепенно увеличиваете размеры дельты с течением времени. Это потребует дополнительной логики / настройки на стороне сервера, но я уверен, что вы сможете понять, если пакеты действительно беспокоят вас.

В идеале вы должны разрешить Delta-Location по URL-адресу запроса, чтобы он вел себя как стандартные заголовки Location и Content-Location для единообразия и гибкости. Один из способов сделать это в JavaScript - это объект URL.

Другие вещи, которые вы можете настроить в этом подходе с гиперссылками:

  • Вероятно, вам следует сделать max-age немного меньше, чем delta, чтобы учесть задержки в сети.
  • Вам может потребоваться дополнительная логика на клиенте, чтобы избежать бесконечного цикла, если сервер (ошибочно) обратится к предыдущей дельте.
  • Вы можете использовать стандартный заголовок Link вместо нестандартного Delta-Location. Но вам все равно понадобится нестандартный тип отношения, поэтому непонятно, что это принесет вам пользу.

Спасибо за хорошие идеи! Я подумаю об этом. Несколько комментариев: +++. Что касается накладных расходов клиента, есть декомпрессия, в которой нужно обработать больше данных (в худшем случае на 200% больше, обычно всего несколько процентов), и замена, которая в основном бесплатна, поскольку мой клиент использует карту ({} в javascript) во всяком случае, внутренне. +++ "Ум" действительно может быть проблемой. Для angular я хочу предоставить HttpInterceptor, который молча выполняет всю работу. +++ Я хотел абстрагироваться от delta у клиентов, но я передумаю.

maaartinus 13.05.2018 05:27

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

maaartinus 13.05.2018 05:29

Другие вопросы по теме