Когда лучше обновлять кэш счетчика лайков: при запросе или при изменении данных
Что проверяют
Понимаете ли вы разницу между сбросом кэша и обновлением счётчика и видите ли гонки при конкурентных лайках. Следом обычно спрашивают, что будет с популярным постом, которому лайки ставят сотни раз в секунду.
Как отвечать
Для счётчика правильный ответ почти всегда «при изменении, но атомарным инкрементом», а не пересчётом:
- инкремент в Redis:
INCR likes:post:{id}при лайке,DECRпри снятии. Операция атомарна, чтение стоит O(1), а в базу значение периодически сбрасывает фоновая задача, например в Celery; - денормализованное поле в базе:
UPDATE posts SET likes_count = likes_count + 1, в Django черезF("likes_count") + 1. Инкремент выполняет сама база, потерянных обновлений нет; - уникальность лайка: ограничение на пару
(user_id, post_id), и счётчик растёт, только если вставка прошла. Иначе двойной клик даёт два лайка; - сверка: время от времени счётчик пересчитывается из таблицы лайков, чтобы дрейф после сбоев не копился.
Ленивый вариант, cache-aside с TTL, подходит, когда пересчёт дешёвый, а отставание на минуту никого не волнует: лайки у старых постов в ленте. Для самого пользователя важнее другое: поставил ли он лайк. Это отдельная проверка по его паре, и она должна быть точной, иначе после клика сердечко погаснет.
Где спотыкаются
Пишут count = cache.get(key) и затем cache.set(key, count + 1) на Python. Два параллельных запроса читают одно значение, и один лайк теряется.
Сбрасывают ключ на каждый лайк у вирусного поста. Кэш почти всегда пуст, и сотни запросов одновременно считают COUNT(*) по большой таблице. Это cache stampede, от него спасают инкремент вместо сброса, блокировка на пересчёт или отдача устаревшего значения.
Не видят гонку cache-aside: читатель взял старое значение из базы, писатель обновил базу и удалил ключ, читатель положил старое значение в кэш. Без TTL такая ошибка живёт вечно.