Ответ
Работая над высоконагруженным 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%.
- Это позволило отложить апгрейд сервера БД, что сэкономило бюджет.
Я задокументировал подход, создал шаблон для команды и провел небольшой воркшоп, после чего стратегия стала стандартом для других «тяжелых» эндпоинтов в проекте.