Код-ревью Senior Кеш и очереди обновлено

Код-ревью: кеш профиля в Redis

Вопрос

Сервис кеширует профиль пользователя в Redis. Найди ошибки в работе с кешем и данными.

async function getProfile(userId) {
  const cached = await redis.get('profile');
  if (cached) return JSON.parse(cached);
  const profile = await db.users.findOne({ id: userId });
  await redis.set('profile:' + userId, JSON.stringify(profile));
  return profile;
}

async function updateProfile(userId, patch) {
  const profile = await getProfile(userId);
  await redis.set('profile:' + userId, JSON.stringify({ ...profile, ...patch }));
  await db.users.update({ id: userId }, patch);
}

async function clearAll() {
  const keys = await redis.keys('profile:*');
  for (const k of keys) await redis.del(k);
}

Коротко

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

Что должен найти ревьюер

  1. Безопасностьстрока 2
    Читается ключ profile, а пишется profile:<id>: кеш никогда не срабатывает, а если ключ profile где-то появится, все пользователи получат один и тот же чужой профиль.
  2. Ошибкастрока 5
    Запись без TTL (EX): при ошибке инвалидации устаревшие данные живут вечно, память Redis растёт.
  3. Ошибкастрока 5
    Если пользователя нет, в кеш кладётся строка null, а при чтении if (cached) она истинна и JSON.parse вернёт null: так получается непреднамеренное вечное отрицательное кеширование.
  4. Ошибкастрока 11
    Кеш обновляется раньше базы: если db.update упадёт, в кеше останутся данные, которых нет в БД. Правильно: сначала БД, потом удалить ключ (не перезаписывать).
  5. Ошибкастрока 10
    Чтение-изменение-запись без блокировки: два параллельных updateProfile затрут изменения друг друга в кеше.
  6. Производительностьстрока 16
    KEYS блокирует однопоточный Redis на всё время обхода ключей. Нужен SCAN с курсором (или UNLINK пачками).
  7. Производительностьстрока 17
    Удаление по одному ключу с await в цикле: N сетевых раундтрипов вместо пачки DEL/pipeline.

Разбор

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

Общий принцип инвалидации: пишем в базу, потом удаляем ключ, следующее чтение подтянет свежее. Перезапись кеша на запись проигрывает гонку: два запроса обновили БД в одном порядке, а кеш в другом. Всегда ставим TTL, чтобы любая ошибка инвалидации самоисправлялась.

KEYS в проде запрещают через rename-command или ACL именно потому, что Redis однопоточный и команда O(N) замораживает все остальные запросы.

Ответьте сами, а не прочитайте

В тренажёре вопрос попадёт в повторения и вернётся, когда начнёт забываться. Плюс билеты с зачётом: 20 вопросов, не больше 2 ошибок.