Как реализовать систему лайков в фотохостинге с хранением данных и оптимизацией запросов
Что проверяют
Умеете ли вы спроектировать простую на вид фичу под нагрузку: где хранить факт лайка, как быстро отдавать счётчик и как не упасть на популярной фотографии. Следом спрашивают, как в ленте из 20 фото показать, какие из них пользователь уже лайкнул.
Как отвечать
Идите от данных к нагрузке:
- таблица
likes(user_id, photo_id, created_at)с первичным ключом на паре(photo_id, user_id). Суррогатныйidне нужен, а уникальность пары делает лайк идемпотентным:INSERT ... ON CONFLICT DO NOTHINGв PostgreSQL. Второй индекс(user_id, created_at)нужен для раздела «понравилось мне»; - счётчик:
COUNT(*)на каждый показ фото дорог. Держите денормализованное полеphotos.likes_countи меняйте его в той же транзакции, только если вставка или удаление действительно затронули строку; - горячие фото: тысячи лайков в секунду на одну запись дают конкуренцию за строку. Решения:
INCRв Redis со сбросом в базу пачками, шардированный счётчик из нескольких строк на фото или очередь событий (Celery, Kafka) с агрегацией. Цена во всех случаях: счётчик отстаёт на секунды, и это стоит согласовать; - «лайкнул ли я» для ленты: один запрос
WHERE user_id = :me AND photo_id = ANY(:ids)на всю страницу, а не по запросу на каждое фото; - API:
PUT /photos/{id}/likeиDELETEна тот же адрес. Оба идемпотентны, повторное нажатие не ломает счётчик. Пользователь берётся из токена сессии, а не из тела запроса.
В Python-стеке это FastAPI или Django, SQLAlchemy или Django ORM, PostgreSQL и Redis. Список лайкнувших отдавайте с курсорной пагинацией по created_at, а не через OFFSET, который на глубоких страницах замедляется.
Где спотыкаются
Проверяют «есть ли лайк» отдельным SELECT, а потом вставляют. Два быстрых нажатия проскакивают между проверкой и вставкой, и без уникального ограничения появляется двойной лайк.
Кладут счётчик только в кеш без источника истины. После перезапуска Redis или вытеснения ключа число лайков обнуляется, и восстановить его можно только полным пересчётом.
Не обрабатывают снятие несуществующего лайка. Код получает None, падает на удалении, а клиент видит 500 вместо спокойного 204.