Зміст
Коротко
Класичний шлях у Node.js: Map у пам’яті → Redis при масштабуванні → гібрид, який ламається о 2:00. На Dev.to автор протестував layercache — багатошаровий кеш із захистом від лавини запитів при простроченні ключа, плавною деградацією при збоях Redis і координацією між екземплярами. На HTTP-бенчмарках пропускна здатність зросла до ~100× порівняно з маршрутом без кешу.
Що сталося
layercache складає рівні: L1 у пам’яті процесу (0,01 ms), L2 Redis (0,5 ms), опційно L3 на диску. При промаху нижній шар зворотно наповнює верхній — потрапляння в L2 автоматично заповнює L1.
Головний біль — «стадо»: ключ прострочився, 75 одночасних запитів б’ють у базу 75 разів. Дедуплікація через координаційне блокування в Redis (або пам’яті) лишає одного завантажувача; решта чекають. У тесті 375 запитів до «холодного» ключа джерело даних викликали рівно один раз на раунд прострочення, а не 75.
На Express + autocannon (40 з’єднань, 8 с): /nocache ~161 зап/с, /memory і /layered ~17k зап/с при 97-му перцентилі близько 4 ms — різниця між «лише пам’ять» і багатошаровим кешем на гарячому шляху майже зникає, зате Redis дає спільний L2 для кількох екземплярів.
При штучній затримці Redis 500 ms гарячі L1-ключі лишалися швидкими; промах по холодному ключу з недоступним Redis усе одно впирався в таймаут (~2 с) — плавна деградація рятує прогріті ключі, але не підміняє джерело для нових.
При maxSize: 25 і 180 об’єктах по 256 KB L1 витісняв старі записи, але при повторному читанні дані підтягувалися з L2, а не з бази. Два екземпляри Node.js зі спільним Redis: скидання кешу через Pub/Sub і 60 паралельних запитів на відсутній ключ → одне звернення до джерела.
Чому це важливо
Самописний кеш рідко закриває лавину запитів і узгодженість між репліками. Без координації горизонтальне масштабування множить навантаження на БД у момент прострочення терміну життя популярного ключа.
layercache не скасовує операційну дисципліну: критичні ключі треба прогріти до інциденту з Redis, а великі об’єкти тримати в щедрому L1 — серіалізація 1 MB у Redis у бенчмарку була ~200× повільніша, ніж попадання в пам’ять.
На практиці
- Для кількох екземплярів — багатошаровий стек (L1 + Redis) з дедуплікацією, а не лише локальний
Map. - Прогрівайте гарячі ключі перед піками; промах по холодному ключу при повільному Redis може коштувати секунди.
- Не розраховуйте на плавну деградацію для нових ключів при повному падінні Redis — лише для вже прогрітого L1.
- Задайте
maxSizeL1 із запасом під великі об’єкти. - Підключіть RedisInvalidationBus і RedisSingleFlightCoordinator для кластера.
const cache = new CacheStack([
new MemoryLayer({ ttl: 60, maxSize: 10_000 }),
new RedisLayer({ client: redis, ttl: 3600 })
])
const user = await cache.get('user:123', () => db.findUser(123))
Підсумок
layercache закриває три продакшен-проблеми — лавину запитів, розподілене скидання кешу й часткову деградацію — без втрати швидкості L1 на гарячому шляху. Для сервісів за межами одного процесу це практичніше, ніж черговий саморобний обгортку над ioredis.