Как лучше хранить просмотры видео: сразу в базе или через кэш с периодической синхронизацией
Что проверяют
Умеете ли вы рассуждать от нагрузки, а не от моды: сколько просмотров в секунду, насколько точным должно быть число и что допустимо потерять. Следом спрашивают, что станет с накопленными просмотрами, если Redis перезапустится до синхронизации.
Как отвечать
Сначала назовите проблему прямой записи. UPDATE videos SET views = views + 1 на популярном ролике упирается в блокировку одной строки, а в PostgreSQL каждое обновление ещё и создаёт новую версию строки: растут WAL и работа autovacuum. Сотни обновлений в секунду по разным строкам база переживёт, тысячи в одну строку уже нет.
Каркас решения с буфером:
- счётчик в Redis:
INCR views:{video_id}илиHINCRBYв общем хэше. Операция атомарная, из Python это одна командаredis-py; - сброс дельты: периодическая задача в Celery beat забирает накопленное атомарно, через
GETDELилиRENAMEключа во временный, и пишет в базу пачкойviews = views + delta; - окно потерь: при AOF с
appendfsync everysecтеряется не больше секунды, без персистентности всё, что не успело сброситься. Для счётчика просмотров это обычно приемлемо, и это стоит проговорить вслух; - уникальные зрители:
PFADDиPFCOUNTна HyperLogLog, погрешность около 0,81% при нескольких килобайтах на ключ; - что считать просмотром: засчитывать после нескольких секунд воспроизведения и не чаще раза на пользователя в окне, иначе счётчик накручивается обновлением страницы.
На большом масштабе события просмотра пишут в Kafka и агрегируют в ClickHouse, а в основной базе хранят только готовое число для показа.
Где спотыкаются
Делают GET, прибавляют единицу и SET. Параллельные запросы затирают друг друга, и просмотры теряются.
Сбрасывают так: прочитали значение, записали в базу, обнулили ключ. Всё, что пришло между чтением и обнулением, пропадает. Нужен атомарный забор.
Синхронизируют перезаписью views = redis_value вместо прибавления дельты. После перезапуска Redis без персистентности счётчик в базе откатывается почти к нулю.