Кэширование конкурентных запросов

Реализуя систему, упёрлись в потолок производительности. Запросы идут, инициализируют достаточно большие выборки, грузят машину. Постепенно время ответа увеличивается, что непозволительно. А экстенсивный путь - не наш путь. 

Основную часть проекта составляет API, над которым и нужно поработать. 

Изучение логов показало, что на бэк приходит большое количество однотипных запросов. И даже больше - они идентичны. Первая мысль - закешировать ответ и отдавать его при следующем запросе. 

Система реализована на PHP/Laravel, что нужно учитывать при рассмотрении примеров. 

Механизм кеширования запросов представляет собой слой (middleware). В самом простом варианте бездумного кеширования, выглядит так: 

public function handle($request, Closure $next) 
{ 
     $queryParams = $request->query(); 
     $queryParams['url'] = $request->path(); 
     ksort($queryParams); 
     $paramHash = md5(serialize($queryParams)); 
     $key = 'responseCache:' . $paramHash; 
     $response = null; 
     if ($request->isMethod('get') && ($response = \Cache::get($key)) === null) { 
         /** @var Response $response */ 
         $response = $next($request); 
         if ($response->isSuccessful()) { 
             try { 
                 \Cache::forever($key, $response); 
             } catch (\Exception $ex) { 
                 \Log::info($ex->getMessage()); 
             } 
         } 
     } elseif ($response === null) { 
         $response = $next($request); 
     } 
     return $response; 
} 

В этом методе происходит формирование ключа, проверка его наличия в кэше. Если ответ уже закэширован, то сразу отдаётся, иначе запрос полноценно обрабатывается и ответ кладётся в кэш. Обрабатываются только GET-запросы. 

Сброс кэша происходит где-то в другом месте. 

Но это очень простой механизм. Могут быть достаточно сложные выборки и большой объем обрабатываемых данных. И если одновременно придёт много запросов, система может лечь на какое-то время, что расстроит её клиентов. Обработка в любом случае должна происходить один раз. 

В такой ситуации будет не плохо знать, что выборка и подготовка данных уже в процессе. Подождать ответа и отдать его сразу всем клиентам. 

Запоминать будем в том же самом кэше. Ставится флаг, который проверяется раз в некоторый период времени. 

Код таков: 

<?php namespace Application\Http\Middleware; 
  
use Closure; 
use Illuminate\Http\Response; 
  
/** 
* Middleware, that adds different headers to response 
* @package Application\Http\Middleware
*/ 
class RequestCache 
{ 
    const SLEEP_TIME = 200000; 
  
    /** 
     * Handle an incoming request. 
     * 
     * @param \Illuminate\Http\Request $request 
     * @param \Closure                 $next 
     * 
     * @return mixed 
     */ 
    public function handle($request, Closure $next) 
    { 
        $params = func_get_args(); 
        $tags = ['response']; 
        if (count($params) > 2) { 
            $tags = array_merge($tags, array_slice($params, 2)); 
        } 
        $queryParams = $request->query(); 
        $queryParams['url'] = $request->path(); 
        ksort($queryParams); 
        $paramHash = md5(serialize($queryParams)); 
        $key = 'responseCache:' . $paramHash; 
        $pendingKey = $key . ':pending'; 
        $response = null; 
        if ($request->isMethod('get') && ($response = $this->getCache($key, $pendingKey, $tags)) === null) { 
            \Cache::tags(array_merge($tags, ['pending']))->put($pendingKey, true, 0.3); 
            /** @var Response $response */ 
            $response = $next($request); 
            if ($response->isSuccessful()) { 
                try { 
                    \Cache::tags($tags)->put($key, $response, 5); 
                } catch (\Exception $ex) { 
                    \Log::info($ex->getMessage()); 
                } 
            } 
            \Cache::tags(array_merge($tags, ['pending']))->forget($pendingKey); 
        } elseif ($response === null) { 
            $response = $next($request); 
        } 
  
        return $response; 
    } 
  
    /** 
     * Get cached data 
     * 
     * @param string $key 
     * @param string $pendingKey 
     * @param array  $tags 
     * 
     * @return Response 
     */ 
    protected function getCache($key, $pendingKey, $tags = []) 
    { 
        while (\Cache::tags(array_merge($tags, ['pending']))->has($pendingKey)) { 
            usleep(self::SLEEP_TIME); 
        } 
        if (\Cache::tags($tags)->has($key)) { 
            return \Cache::tags($tags)->get($key); 
        } 
  
        return null; 
    } 
} 

Приложение не может ждать вечно. Вполне возможно, что процесс, установивший флаг, отвалился. Поэтому у флага стоит время жизни. 

После подключения, путям можно указывать middleware: 

Route::group(['middleware' => ['api.requestCache:data']], function () {  
    Route::get('data', 'DataController@index'); 
}); 

api.requestCache - алиас прослойки 

data - тэг 

Чтобы упростить работу с закешированной информацией, стоит разделять её тегами. Например, можно будет прицельно производить очистку. Обновился какой-нибудь справочник - удаляем записи только с тегом reference, обновился профиль пользователя, почистили записи с тегом users. 



Комментарии

добавить
Комментариев пока нет. Будете первым?
Чтобы комментировать, нужно авторизоваться

Советуем почитать


PHP-FPM vs Fast-CGI
FD 0

PHP-FPM vs Fast-CGI читать далее

Common Gateway Interface (CGI) позволяет HTTP-серверу и сценарию CGI делиться ответственностью за запросы клиентов. Сервер ответственен за работу с соединениями, передачу данных, транспорт и различные сетевые задачи. CGI, в свою очередь, управляет всеми процессами, связанными с приложением, такими как доступ к данным и подготовка документов.

0 03.01.2018 17:47:25

Федеральная система
Сергей 0

Федеральная система "Город" читать далее

В прошлый раз описал процесс работы с платёжной системой Cyberplat, теперь хочу поделиться опытом работы с ФСГ (Федеральная система город).

Разработано сие чудо ЦФТ. Старались делать все по ГОСТ, поэтому произвести интеграцию не так просто, как хотелось бы (рассматриваем PHP).

0 11.07.2016 17:40:15