@TochnoNeVadim

Актуальность данных при кэшировании, решается ли?

Добрый день. При проектировании связи MySQL и Redis как кэша, столкнулись с проблемой потери актуальных данных. Много текста, но возможно кто-то когда-то сталкивался с подобной заминкой?

Есть User, с аватаркой и рамкой вокруг неё. У User-а рамка хранится по ID, а url её файла тянем с другой таблицы.
Есть открытые тикеты, и API для фронтенда возвращает структуру полученную из нескольких Join-ов.
> Ticket ID, Заголовок тикета, Имя пользователя, URL на аватарку, URL на рамку (не её ID, а именно URL).

Проблемы:

1. Ещё есть и АPI на получение информации о отдельном пользователе (обычный /api/users/:id) - которое тоже кэширует результат. При изменении картинки рамки администратором - кэш пользователя становится невалидным. Проверять всех пользователей у которых есть эта рамка, чтобы потом вычистить из кэша - кажется как-то накладно (больше пользователей - больше времени на проверку и чистку кэша).

2. При изменении чего либо в тикетах - кэш обновляется. Но если пользователь меняет никнейм, ID выбранной рамки или аватарку - кэш становится неактуальным. Так же, если администратор изменит изображение рамки - URL рамки в кэше опять же остаётся старым.

Мысли:

Внутри команды было предложение взять MongoDB и использовать "references" (что-то вроде ссылок). Но это не даёт особых преимуществ перед MySQL и теми же Join-ами. По скорости ответа в сравнении с MySQL разницы почти нет.

Варианты с другими БД тоже не дадут (вероятно) никакого эффекта, только если не брать Tarantool. Но это отметается потому что с ним и опыта нет. Разработка, поддержка и мониторинг абсолютно незнакомой БД - будут проблемными.

Пробовали отказаться от Join-ов и собирать ответ из предварительно кэшированных фрагментов - получается рабочая но медленная схема (каждый тикет тянет User-а, а тот тянет информацию рамки по её ID). Выборка из 20 тикетов сделает 40 запросов к кэшу (User и рамка). Если кэш холодный - +40 запросов к БД. Если предварительно прогреть кэш, то всё равно 40 к кэшу и 20 к БД. А выборка может быть и из 100 тикетов.

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

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

Был бы рад если бы кто-то поделился опытом, своим решением подобной проблемы, может быть даже ответом с теорией, или ссылками на какие-то ресурсы с чем-то похожим.
  • Вопрос задан
  • 282 просмотра
Решения вопроса 1
@TochnoNeVadim Автор вопроса
За 11 часов рассмотрели подобный вариант, для того чтобы можно было сбросить кэш пользователя из-за изменения рамки администратором. Кажется максимально правильным, но сомнения огромные:

Когда user:user_id попадает в Redis - делаем запись в:
frames:at_user:user_id - отдельно сохранённый ID рамки пользователя.
frames:at_users:frame_id - SET список с ID пользователей, у которых эта рамка есть.

Когда user:user_id удаляется из Redis (del / expired), смотрим - существует ли frames:at_user:user_id. Если существует, то удаляем и его, и удаляем ID пользователя из SET-а frames:at_users:frame_id.

Итог:
При любом обновлении рамки, мы уже можем узнать у каких user-ов она есть (из тех кто в кэше) по SET-у frames:at_users:frame_id и зачистить его параллельно кэшу каждого юзера.

----------------

Кажется что это максимально правильный вариант на тот момент, когда администратор обновит изображение рамки. Есть возможность составить точный список ключей, которые надо очистить. И это число не превысит кол-во юзеров в кэше.

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

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

---------------

UPDATE

В целом, поковыряв ещё немного, решение в этом комментарии можно заменить используя RedisJSON и RediSearch. Накинуть индексы на поля пользователя, и потом вытянуть те ключи - которые нужно дропнуть.

FT.CREATE idx:user
ON JSON PREFIX
1 "users:id:"
SCHEMA
$.frame.id AS frame_id NUMERIC


FT.SEARCH idx:user "@frame_id:[13 13]"


В данный момент предполагаю что это решение можно применить для всей проблемы.
Ответ написан
Комментировать
Пригласить эксперта
Ответы на вопрос 2
sergey-gornostaev
@sergey-gornostaev
Седой и строгий
«There are only two hard things in Computer Science: cache invalidation and naming things» — Phil Karlton

Насколько я понял проблему, кэш проблемных данных нужно хранить в отдельном ключе редиса, по которому его легко найти и инвалидировать из разных мест.
Ответ написан
Комментировать
firedragon
@firedragon
Не джун-мидл-сеньор, а трус-балбес-бывалый.
{
  "TID": "23",
  "Label": "label",
  "Name": "user name",
  "UserId": "666",
  "AvatarId": "42",
  "AvatarUrl": "https://site.com/api/avatar/42",
  "BorderId": "142",
  "BorderUrl": "https://site.com/api/border/142"
}


Предлагаю вам поменять структуру. Вот это основной кэш как видите передача полных url Совершенно лишняя.
Дальше в апи добавить методы обновления кэша.

То есть пользователь меняет
Но если пользователь меняет никнейм, ID выбранной рамки или аватарку - кэш становится неактуальным. Так же, если администратор изменит изображение рамки - URL рамки в кэше опять же остаётся старым.


То делаем следующее
https://site.com/api/tickets/91/update
https://site.com/api/users/666/update
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы