Код-ревью: кеш профиля в 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);
}
Коротко
Что должен найти ревьюер
- Безопасностьстрока 2Читается ключ
profile, а пишетсяprofile:<id>: кеш никогда не срабатывает, а если ключprofileгде-то появится, все пользователи получат один и тот же чужой профиль. - Ошибкастрока 5Запись без TTL (
EX): при ошибке инвалидации устаревшие данные живут вечно, память Redis растёт. - Ошибкастрока 5Если пользователя нет, в кеш кладётся строка
null, а при чтенииif (cached)она истинна иJSON.parseвернёт null: так получается непреднамеренное вечное отрицательное кеширование. - Ошибкастрока 11Кеш обновляется раньше базы: если
db.updateупадёт, в кеше останутся данные, которых нет в БД. Правильно: сначала БД, потом удалить ключ (не перезаписывать). - Ошибкастрока 10Чтение-изменение-запись без блокировки: два параллельных
updateProfileзатрут изменения друг друга в кеше. - Производительностьстрока 16
KEYSблокирует однопоточный Redis на всё время обхода ключей. НуженSCANс курсором (илиUNLINKпачками). - Производительностьстрока 17Удаление по одному ключу с
awaitв цикле: N сетевых раундтрипов вместо пачкиDEL/pipeline.
Разбор
Самая опасная строка вторая: ключ чтения не совпадает с ключом записи. Сейчас это просто бесполезный кеш, но такая опечатка легко превращает кеш в источник утечки чужих персональных данных. Это реальный класс инцидентов: ключ кеша обязан включать всё, от чего зависит ответ (пользователь, локаль, права).
Общий принцип инвалидации: пишем в базу, потом удаляем ключ, следующее чтение подтянет свежее. Перезапись кеша на запись проигрывает гонку: два запроса обновили БД в одном порядке, а кеш в другом. Всегда ставим TTL, чтобы любая ошибка инвалидации самоисправлялась.
KEYS в проде запрещают через rename-command или ACL именно потому, что Redis однопоточный и команда O(N) замораживает все остальные запросы.
Ответьте сами, а не прочитайте
В тренажёре вопрос попадёт в повторения и вернётся, когда начнёт забываться. Плюс билеты с зачётом: 20 вопросов, не больше 2 ошибок.
Предыдущий вопрос
Дедупликация событий из NDJSON-потока
Следующий вопрос
Отложенные задачи: напоминание через 24 часа