Ответ
Работая над высоконагруженным REST API для мобильного приложения, я заметил, что несколько ключевых эндпоинтов (например, GET /api/v1/user/profile и GET /api/v1/catalog/items) генерируют идентичные SQL-запросы при каждом вызове. Это создавало избыточную нагрузку на базу данных MySQL, особенно в часы пик, и увеличивало время отклика.
Моя инициатива: Я предложил и самостоятельно реализовал двухуровневую стратегию кэширования с использованием Redis, предварительно согласовав план с тимлидом.
Что было сделано:
- Анализ: С помощью Laravel Telescope и slow query log выявил самые частые и «тяжелые» запросы.
- Проектирование: Решил кэшировать не только сырые данные из БД, но и готовые ответы API (сериализованные JSON) для эндпоинтов с низкой волатильностью данных.
-
Реализация:
Уровень 1: Кэширование запросов к БД (Query Cache)
// app/Repositories/UserRepository.php class UserRepository { public function getProfileWithStats(int $userId): array { $cacheKey = "user.profile.{$userId}"; $ttl = 3600; // 1 час return Cache::remember($cacheKey, $ttl, function () use ($userId) { return DB::table('users') ->select('users.*', DB::raw('COUNT(orders.id) as order_count')) ->leftJoin('orders', 'users.id', '=', 'orders.user_id') ->where('users.id', $userId) ->groupBy('users.id') ->first(); // Этот сложный join кэшируется }); } }Уровень 2: Кэширование ответа API (Response Cache)
// app/Http/Controllers/API/UserController.php class UserController extends Controller { public function profile(Request $request) { $user = $request->user(); $cacheKey = "api.response.v1.user.profile.{$user->id}"; $ttl = 300; // 5 минут, т.к. данные профиля могут обновляться // Если в кэше есть готовый JSON-ответ, возвращаем его сразу if (Cache::has($cacheKey)) { return response(Cache::get($cacheKey))->header('Content-Type', 'application/json'); } // Иначе выполняем логику и кэшируем результат $data = $this->userService->getProfileData($user); $response = response()->json($data); Cache::put($cacheKey, $response->getContent(), $ttl); return $response; } }Уровень 3: Инвалидация кэша
// В местах обновления данных (например, в сервисе обновления профиля) public function updateUserProfile(User $user, array $data): void { $user->update($data); // Чистим оба уровня кэша при изменении Cache::forget("user.profile.{$user->id}"); Cache::forget("api.response.v1.user.profile.{$user->id}"); }
Результат:
- Среднее время отклика проблемных эндпоинтов сократилось с ~120мс до ~15мс.
- Нагрузка на базу данных (измеряемая в операциях чтения в секунду) снизилась на ~40%.
- Это позволило отложить апгрейд сервера БД, что сэкономило бюджет.
Я задокументировал подход, создал шаблон для команды и провел небольшой воркшоп, после чего стратегия стала стандартом для других «тяжелых» эндпоинтов в проекте.
Ответ 18+ 🔞
Да ты только послушай, какая история приключилась! Сижу я, значит, на этом проекте, где мобильное приложение с REST API, которое под нагрузкой просто пиздец как тупило. Открываю логи, смотрю — а там, ёпта, одни и те же SQL-запросы, как заевшая пластинка, на каждый чих в базу летят. Эндпоинты GET /api/v1/user/profile и GET /api/v1/catalog/items просто жрали ресурсы, особенно когда народ активничал. Нагрузка на MySQL была — ядерёна вошь, время отклика под 120 мс — волнение ебать, терпения ноль.
И вот сижу, думаю: «Э, бошка, думай! Так же нельзя, накроется всё медным тазом». Подозрение ебать чувствую, что тут без хитрой жопы не обойтись. Решил — буду кэшировать. Согласовал с тимлидом, мол, да похуй на сомнения, надо делать.
Что я отмочил:
- Анализ. Взял Laravel Telescope и логи медленных запросов, начал выискивать, где собака зарыта. Нашёл, сука, самых прожорливых.
- Проектирование. Решил не мелочиться — кэшировать буду на два фронта. И сырые данные из БД, и сразу готовые JSON-ответы от API, чтобы вообще не париться, если данные редко меняются.
-
Реализация. Тут, бля, началась магия.
Уровень 1: Кэш для запросов к БД (Query Cache). Взял эти дурацкие join'ы с группировками и спрятал их в
Cache::remember. Теперь вместо того чтобы каждый раз взъёбывать базу, данные лежат в Redis.// app/Repositories/UserRepository.php class UserRepository { public function getProfileWithStats(int $userId): array { $cacheKey = "user.profile.{$userId}"; $ttl = 3600; // 1 час return Cache::remember($cacheKey, $ttl, function () use ($userId) { return DB::table('users') ->select('users.*', DB::raw('COUNT(orders.id) as order_count')) ->leftJoin('orders', 'users.id', '=', 'orders.user_id') ->where('users.id', $userId) ->groupBy('users.id') ->first(); // Этот сложный join кэшируется }); } }Уровень 2: Кэш для целого ответа API (Response Cache). А это, бля, вообще овердохуища эффективно. Если в Redis уже лежит готовый JSON, который контроллер должен отдать, мы его просто выплёвываем, минуя всю бизнес-логику. Красота!
// app/Http/Controllers/API/UserController.php class UserController extends Controller { public function profile(Request $request) { $user = $request->user(); $cacheKey = "api.response.v1.user.profile.{$user->id}"; $ttl = 300; // 5 минут, т.к. данные профиля могут обновляться // Если в кэше есть готовый JSON-ответ, возвращаем его сразу if (Cache::has($cacheKey)) { return response(Cache::get($cacheKey))->header('Content-Type', 'application/json'); } // Иначе выполняем логику и кэшируем результат $data = $this->userService->getProfileData($user); $response = response()->json($data); Cache::put($cacheKey, $response->getContent(), $ttl); return $response; } }Уровень 3: Инвалидация — без неё никуда. А то получится пиздопроебибна ситуация: данные у пользователя обновились, а в кэше старые. Поэтому в местах, где что-то меняем, чистим оба ключа разом.
// В местах обновления данных (например, в сервисе обновления профиля) public function updateUserProfile(User $user, array $data): void { $user->update($data); // Чистим оба уровня кэша при изменении Cache::forget("user.profile.{$user->id}"); Cache::forget("api.response.v1.user.profile.{$user->id}"); }
И что в итоге, спросишь ты? А итог — ни хуя себе!
- Время отклика этих эндпоинтов упало со 120 мс до 15. Ебааать!
- Нагрузка на чтение с базы просела на 40%. База вздохнула свободно.
- И самое вкусное — апгрейд сервера БД отложили. Бюджет сэкономили, начальство довольно. В рот мне чих-пых, красота!
Ну и, конечно, я не стал это держать в себе. Оформил всё как надо, задокументировал, ребятам из команды показал, как это работает. Теперь этот подход у нас как стандарт для всех тормозных мест. Вот так, чувак, иногда нужно просто сесть и подумать, а не вздрачивать базу на ровном месте.