Практика: настройка кеширования

В прошлом уроке вы рассмотрели паттерны кеширования по работе с данными и способы инвалидации кеша. А этот урок посвящён практике: вы научитесь настраивать паттерны для чтения — Cache-Aside, Read-Through, чтобы не заниматься настройкой базы данных.
Для реализации паттернов кеширования вы поработаете с сервисом Redis, а также Node.js как самым доступным способом запуска Redis — один из самых популярных инструментов кеширования.

Реализация Cache-Aside

  1. Воспользуйтесь доступным API https://restcountries.com/v3.1/all?fields=name,flags, которое возвращает имя страны и флаг.
  2. Зайдите на сайт https://nodejs.org и установите инструмент Node.js.
  3. Дальше в консоли выполните следующие команды, чтобы установить необходимые пакеты:

    BASH

    npm install express
    mkdir cache-sample
    cd cache-sample 
  4. И пакет для кеширования — https://github.com/node-cache/node-cache:

    BASH

    npm install node-cache --save
    npm install node-fetch 
  5. Теперь можно писать код:

    BASH

    import express from 'express';
    import fetch from 'node-fetch';
    import NodeCache from 'node-cache';
    
    // stdTTL is the default time-to-live for each cache entry
    const cache = new NodeCache({ stdTTL: 300 });
    
    // retrieve data
    async function getCountries() {
     const response = await fetch(`https://restcountries.com/v3.1/all?fields=name,flags`);
    
     if (!response.ok) {
       throw new Error(response.statusText);
     }
    
     return await response.json();
    }
    
    const app = express(); 
    app.get('/countries', async (req, res) => {
     try {
       // try to get the countries from cache
       let countries = cache.get('allCountries');
    
       // if data from cache is empty then get data from store
       if (countries == null) {
         countries = await getCountries();
         // time-to-live is set to 300 seconds. 
         cache.set('allCountries', countries, 300);
       }
    
       res.status(200).send(countries);
     } catch (err) {
       console.log(err);
       res.sendStatus(500);
     }
    });
    
    const port = 3000;
    app.listen(port, () => {
    console.log(`Server listening on http://localhost:${port}`);
    }); 
  6. Рядом создайте файл package.json:

    JSX

    {
       "type": "module"
    } 
  7. Запускайте и проверяйте. Для проверки можно воспользоваться утилитой Postman, которой удобно смотреть время ответа. Или сделать то же в браузере через консоль разработчика F12.

    BASH

    node index.js 
Взгляните: первый запрос http://localhost:3000/countries выполняется достаточно долго из-за большого объёма данных:
Зато следующий проходит почти моментально, потому что данные приходят из кеша.
10 ms против 546ms — это ощутимая разница.
Но сейчас данные кеша хранятся в памяти и при остановке сервиса всё очистится. Попробуем добавить Redis.

Кеширование с Redis

  1. Создайте в другой папке файл docker-compose.yaml:

    YAML

     services:
       redis:
         image: 'redis:alpine'
         ports:
           - '6379:6379'
      
  2. Запустите:

    YAML

     docker-compose up -d
      
  3. Теперь можно скопировать предыдущий скрипт и package.json, чтобы внести в него изменения, поставить библиотеки:

    YAML

     npm install redis express node-fetch
      
  4. Теперь нужно исправить сам скрипт, чтобы добавить новый функционал. Он будет выглядеть вот так:

    TSX

     import express from 'express';
     import fetch from 'node-fetch';
     import { createClient } from 'redis';
     
     const client = await createClient()
       .on('error', err => console.log('Redis Client Error', err))
       .connect();
     
     // извлечение данных
     async function getCountries() {
       
       const response = await fetch(`https://restcountries.com/v3.1/all?fields=name,flags`);
     
       if (!response.ok) {
         throw new Error(response.statusText);
       }
     
       return await response.json();
     }
     
     const app = express(); 
     app.get('/countries', async (req, res) => {
       try {
         // попробуем извлечь данные о странах из кеша
         let countries = null;
         try {
           // попробуем извлечь данные о странах из кеша
           countries = await client.get('countries');
         } catch (err) {
           console.log(err);
         }
         // если кеш пуст, извлекаем данные из хранилища
         if (countries == null) {
           countries = await getCountries();
           await client.set('countries', JSON.stringify(countries));
         }
     
         res.status(200).send(JSON.stringify(countries));
       } catch (err) {
         console.log(err);
         res.sendStatus(500);
       }
     });
     
     const port = 3000;
     app.listen(port, () => {
       console.log(`Server listening on http://localhost:${port}`);
     });
      
  5. Запуск!

    BASH

     node index.js
      
Если первый запрос выполняется так же долго, то второй и последующие даже при перезапуске сервиса будут выполняться быстро.
Посмотрим вариант, как в фоне положить актуальные данные в кеш, чтобы не ждать данных при вызове.

Паттерн Refresh-Ahead

  1. Скопируйте последний скрипт.
  2. Здесь нужно установить зависимости:

    BASH

      npm install redis express node-fetch
      
  3. И запустить Redis, если его не было:

    BASH

     docker-compose up -d
      
  4. Чтобы данные загружались в кеш в фоне и запросы шли только к кешу для быстрого отклика, немного подправьте скрипт. Вот так:

    JSX

     import express from 'express';
     import fetch from 'node-fetch';
     import { createClient } from 'redis';
     
     const client = await createClient()
       .on('error', err => console.log('Redis Client Error', err))
       .connect();
     
     // извлечение данных
     async function getCountries() {
       
       const response = await fetch(`https://restcountries.com/v3.1/all?fields=name,flags`);
     
       if (!response.ok) {
         throw new Error(response.statusText);
       }
     
       return await response.json();
     }
     
     const app = express(); 
     app.get('/countries', async (req, res) => {
       try {
         // попробуем извлечь данные о странах из кеша
         let countries = null;
         try {
           // попробуем извлечь данные о странах из кеша
           countries = await client.get('data');
         } catch (err) {
           console.log(err);
         }
         res.status(200).send(JSON.stringify(countries));
       } catch (err) {
         console.log(err);
         res.sendStatus(500);
       }
     });
     
     setInterval(async () => {
       console.log('Running a scheduled task');
       let countries = await getCountries();
       updateData('data', JSON.stringify(countries));
     }, 2000); //every 2 seconds
     
     // определение функции для обновления данных, 
     // определение приоритетности обновлений кеша 
     // и асинхронной отсрочки обновлений хранилища данных
     async function updateData(key, newData) {
       // инициируются обновления кеша
       await client.set(key, JSON.stringify(newData));
       console.log('cache updated');
     }
     
     const port = 3000;
     app.listen(port, () => {
       console.log(`Server listening on http://localhost:${port}`);
     });
      
  5. Теперь, если данных нет, то приложение получит пустой ответ. Но с учётом обновления данных каждые пару секунд данные всегда будут актуальными.

Итоги

Кеширование значительно улучшает метрики и повышает пропускную способность сервиса. Главное — не забывать про его минусы и использовать кеш по назначению.
В следующем уроке вы узнаете, как поступать, когда нагрузка на приложение сильно возрастает. Для поддержания высоконагруженных приложений используются паттерны Backpressure и Curcuit — о них и пойдёт речь.