MySQL vs Redis: Write-through проти Cache-aside — що ламається рідше в реальних застосунках

Було корисно?

У стенді все швидко. Потім з’являється продакшн із 99-м перцентилем, що нагадує гірський схил, і кластер Redis, який «в порядку», поки раптом ним не перестає бути. Хтось додає кеш, щоб полегшити гарячу точку MySQL. Гаряча точка переміщується. Потім виклик на пейджер приходить у вашу спальню.

Неромантична правда така: MySQL і Redis не конкурують у реальних системах — вони співпрацюють. Ваше завдання — не допустити, щоб вони тихо не погоджувалися щодо реальності. Ось що таке write-through і cache-aside: контракти між вашим застосунком, кешем і базою даних. Один із контрактів ламається рідше — якщо ви обрали його з правильних причин і експлуатуєте його відповідально.

The real question: what breaks less

Справжнє питання: що ламається рідше

«MySQL vs Redis» — це фейкове суперництво. MySQL — ваша система істини. Redis — ваша ставка на продуктивність. Питання, яке має значення: який патерн кешування призводить до меншої кількості помітних для користувача відмов під час звичайного операційного хаосу — деплоїв, часткових відмов, спайків латентності та випадкових «хтось виконав команду в невірному терміналі» подій.

Якщо хочете думку одразу:

  • Cache-aside ламається рідше для більшості продуктових застосунків, бо він відмовляє «відкрито»: коли Redis незадоволений, ви все ще можете читати й записувати в MySQL і виживати.
  • Write-through ламається рідше лише коли ви інвестуєте в жорсткі операційні гарантії: дисципліновані таймаути, черги або ретраї з идемпотентністю та чіткі правила на випадок недоступності Redis або MySQL.
  • Write-through ламається голосніше. Це не завжди погано. Голосні відмови легше дебажити. Тиха неконсистентність — от де ви втрачаєте вихідні дні.

Без безкоштовного обіду: є лише місце, де ви хочете платити. Cache-aside зазвичай оплачується інколи застарілими читаннями і стампеїдами. Write-through платиться затримкою write-шляху і більшою кількістю способів заклинити весь застосунок, якщо кеш підвисає.

Одна цитата, щоб тримати її на моніторі — вона врятувала багато on-call змін (ідейно переказано): Werner Vogels: будуй з урахуванням відмов — передбачай, що все ламається, і проектуй так, щоби це не перетворилося на катастрофу.

Definitions you can deploy

Визначення, які можна застосувати

MySQL

Стійка реляційна база даних. Дурне збереження, транзакційна семантика та гнучкі запити. Також: річ, у яку всі звинувачують, коли застосунок повільний — навіть якщо проблема в мережі, пулі або в дизайні ключів кешу, що перетворився на перформанс-арт.

Redis

Магазин структур даних в пам’яті. Використовується як кеш, черга, лімітатор швидкості, сховище сесій і «тимчасова база даних, ми обіцяємо, що це не база даних». Redis може зберігати дані на диск (RDB снапшоти, AOF логи), може реплікуватися й шардуватися, але це інший звір порівняно з MySQL: він оптимізований під швидкість і простоту, а не під реляційну правильність.

Cache-aside (ліниве завантаження)

Застосунок відповідає за читання з кешу та наповнення кешу:

  1. Шлях читання: перевірка Redis → якщо промах, читати MySQL → записати в Redis → повернути.
  2. Шлях запису: записати в MySQL → інвалідовувати або оновити Redis.

Ключовий момент: кеш є опціональним. Якщо Redis падає, ви можете оминати його та звертатися до MySQL. Ваш найгірший день стає «повільним», а не «померлим», якщо MySQL може прийняти навантаження.

Write-through (синхронне наповнення)

Застосунок записує в кеш і базу як одну логічну операцію. Варіанти відрізняються, але сутність така:

  1. Шлях запису: запис у Redis (або шар кешу) і MySQL у складі одного запиту.
  2. Шлях читання: читання з Redis; кеш має бути гарячим і коректним.

Ключовий момент: кеш стає частиною правильності. Якщо Redis нездоровий, ваш шлях запису постраждає. Якщо ваш write-through шар брешe, ваш застосунок бреше.

Короткий жарт #1: Write-through — як «швидка» заявка на зміну у великій компанії — швидко, поки не почнуться погодження.

Facts & history that actually matter

Факти та історія, що дійсно мають значення

  • Redis почався (2009) як практична відповідь на повільний доступ до даних у вебзастосунках, а не як універсальна платформа. Його ДНК «робити прості речі надзвичайно швидко» досі помітна.
  • Memcached популяризував cache-aside у мейнстрімі. Багато звичок кешування з Redis успадковані з тієї епохи: TTL скрізь, інвалідування за принципом best-effort і толерантність до випадкової неконсистентності.
  • MySQL query cache був видалений (MySQL 8.0), бо він створював контеншн і непередбачувану продуктивність. Кешування перемістилося в прикладні шари та виділені кеші.
  • Redis одно-потоковий для виконання команд (з деяким багатопотоковим I/O у новіших версіях). Ось чому він швидкий і передбачуваний — поки ви не запускаєте повільні команди, що блокують світ.
  • Персистентність Redis опціональна та налаштовувана: RDB снапшоти міняють надійність на швидкість; AOF додає навантаження на диск заради тоншого відновлення. Вибір змінює те, що саме означає «write-through».
  • Реплікація за замовчуванням асинхронна в Redis і в багатьох топологіях MySQL. «Я записав» може означати «праймірі прийняв запис», а не «запис захищено від втрати».
  • Cache stampede (thundering herd) відомий ще десятиліттями у великих вебсистемах; пом’якшення — request coalescing і jittered TTL — старі ідеї, які досі ігнорують щотижня.
  • Redis Cluster шардує за hash slot ключа. Операції з кількома ключами швидко ускладнюються, а між-слотові операції можуть стати мовчазною пасткою для write-through робочих потоків.

Write-through vs cache-aside: decision-grade comparison

Write-through vs cache-aside: порівняння для прийняття рішень

What you’re optimizing for

За що ви оптимізуєте

Якщо ваша основна біль — латентність читань і набір даних відносно стабільний, cache-aside зазвичай достатній. Якщо ваша основна проблема — ампліфікація читань через складні обчислені об’єкти (наприклад, зібраний профіль користувача + права + лічильники) і ви хочете постійно гарячий кеш, write-through стає привабливішим.

Але не плутайте «гарячий» з «коректним». Гарячий — просто. Коректність — де приходить рахунок.

Operational blast radius

Операційний радіус ураження

  • Cache-aside: відмова Redis → зростання навантаження на MySQL → можливе насичення MySQL → повільна або часткова відмова. Ви можете деградувати поступово, якщо підготувалися.
  • Write-through: відмова Redis → шлях запису може блокуватися або падати → каскадні відмови в рівні застосунків → outage навіть якщо MySQL у порядку.

Consistency profile

Профіль консистентності

Жоден з патернів не дає транзакційної консистентності між MySQL і Redis без додаткових механізмів. Потрібно вибрати, яку неконсистентність ви готові терпіти:

  • Cache-aside ризикує застарілими читаннями після записів (гонки інвалідування, затримка реплікації, пропущені видалення).
  • Write-through ризикує роздвоєною правдою, якщо один запис пройшов, а інший не пройшов або ретраї спотворили порядок. Це не застаріле — це суперечливе.

Latency profile

Профіль латентності

Cache-aside тримає шлях запису в межах MySQL, який ви вже налагодили. Write-through додає Redis у шлях запису. Якщо Redis у іншій зоні доступності, за TLS або просто завантажений, вітаємо: ви перетворили проблему локального диска на розподілену системну проблему.

When I pick cache-aside

Коли я обираю cache-aside

  • Навантаження з домінуванням читань та прийнятною eventual-consistency.
  • Об’єкти, які можна відтворити з MySQL на вимогу.
  • Команди, які хочуть мати можливість обходити кеш під час інцидентів.
  • Моделі даних з частими записами та складною інвалідацією все ще працюватимуть — якщо правила прості.

When I pick write-through

Коли я обираю write-through

  • У вас є чіткий шар кешування/сервіс, який володіє контрактом write-through і може експлуатуватися як компонент бази даних.
  • Ви можете гарантувати идемпотентність і впорядкування записів (або терпіти last-write-wins).
  • Ви готові бюджетувати латентність та доступність для Redis як для критичної інфраструктури.
  • Ви хочете передбачуваного гарячого кешу для читань і можете тримати структури даних простими.

Failure modes: how each pattern dies

Режими відмов: як кожен патерн помирає

Cache-aside: the classics

Cache-aside: класика

1) Stale reads from invalidation races

1) Застарілі читання через гонки інвалідування

Типова послідовність:

  1. Запит A читає кеш-промах, дістає рядок з MySQL (старе значення), потім готується встановити Redis.
  2. Запит B оновлює рядок у MySQL (нове значення), видаляє ключ у Redis.
  3. Запит A тепер встановлює Redis зі старим значенням після видалення B.

Результат: Redis містить застарілі дані, поки не закінчиться TTL або не відбудеться наступна інвалідизація. Клієнти бачать старий стан. Інженери чують «але ми видалили ключ». Обидва твердження вірні.

Пом’якшення: версіоновані ключі, compare-and-set (Lua скрипт з версією) або запис оновлень у кеш з монотонно зростаючою версією/таймстемпом.

2) Cache stampede after expiry

2) Stampede кешу після закінчення TTL

Гарячий ключ спливає. Тисячі запитів одночасно промахуються. Всі йдуть в MySQL. MySQL падає. Redis дивиться на це як кіт на лазерну крапку.

Пом’якшення: request coalescing (single flight), імовірнісне раннє оновлення, per-key mutex, джиттер TTL та «подавати застаріле під час повторної валідації».

3) Cache penetration (misses for non-existent keys)

3) Пробивання кешу (промахи для неіснуючих ключів)

Атаковий трафік або баги в клієнтах роблять запити ID, яких не існує. Промахи в кеші не кешуються, тож MySQL обстрілюється марними запитами.

Пом’якшення: negative caching з коротким TTL, bloom-фільтри, ліміти швидкості.

4) Silent partial failures

4) Тихі часткові відмови

Таймаути Redis не трактують як відмови. Застосунок чекає занадто довго і займає потоки. Або застосунок агресивно ретраїть і стає DoS-ом для себе.

Пом’якшення: суворі таймаути, circuit breaker і чітка семантика «кеш — best-effort».

Write-through: fewer misses, more correctness traps

Write-through: менше промахів, більше пасток консистентності

1) Dual-write inconsistency

1) Неконсистентність при подвійному записі

Запис у Redis пройшов; запис у MySQL впав. Або навпаки. Або обидва пройшли, але ретраї змінили порядок. Тепер кеш і БД не погоджуються, а ваш шлях читання гарантовано віддає щось — можливо, неправильне.

Пом’якшення: transactional outbox, change-data-capture (CDC) для рушійного оновлення кешу, або робити MySQL авторитетним і вважати запис у кеш лише похідним.

2) Latency amplification on the write path

2) Ампліфікація латентності на шляху запису

Підвисання Redis (повільний диск для AOF fsync, мережевий джиттер, CPU-спайки). Write-through перетворює це на видиму для користувача латентність запису. Таймаути спричиняють ретраї. Ретраї — навантаження. Навантаження — більше таймаутів. Ви знаєте далі.

3) Redis persistence surprises

3) Сюрпризи з персистентністю Redis

Якщо ви покладаєтеся на Redis як частину write-through правильності, але Redis налаштований лише на снапшоти, крах може втратити останні записи. MySQL може бути коректним; Redis — у минулому. Якщо ваш застосунок читає Redis першим, ви почнете видавати подорожі в часі.

4) Cluster topology + key design pitfalls

4) Пастки топології кластера та дизайну ключів

Write-through часто хоче атомарності по кількох ключах (оновлення об’єкта + індексу + лічильників). Redis Cluster не може виконувати мульти-ключові транзакції через hash slots. Люди обходять це hash tags, потім виявляють, що створили гарячу шардову точку.

Короткий жарт #2: Інвалідизація кешу — одна з важких проблем, але принаймні вона не має зборів. Подвійні записи мають.

Three corporate mini-stories from the trenches

Три корпоративні міні-історії з полів бою

Incident #1: the outage caused by a wrong assumption

Інцидент №1: відмова через неправильне припущення

Середнього розміру SaaS-компанія використовувала класичну cache-aside топологію: MySQL primary з репліками, Redis для гарячих об’єктів. Команда припустила «читання з Redis дешеві, тож можна використовувати їх усюди». Вони розкидали виклики Redis по коду — фіч-флаги, ліміти, сесії користувачів і кілька критичних перевірок авторизації.

Потім стався регіональний мережевий інцидент, який збільшив втрата пакетів між шаром застосунку і вузлами Redis. Redis сам по собі був здоровий. Латентність навіть не була страшною — просто джиттер з таймаутами. Redis клієнт застосунку мав таймаут 2 секунди і ретраї за замовчуванням. Під навантаженням потоки накопичувалися в очікуванні Redis. Зрештою веб-воркери досягли максимуму конкурентності і перестали приймати запити.

MySQL був у порядку. CPU був у порядку. Інцидент-лейдер постійно чув «але Redis працює». Звісно. Сервер може бути «up» так само, як двері можуть бути «closed». Обидва технічно вірні і операційно марні.

Виправлення не було героїчним. Вони зменшили таймаути Redis до реалістичних значень (десятки мілісекунд, не секунди), додали circuit breaker і — що ключове — перестали використовувати Redis як жорстку залежність для рішень авторизації. Для авторизації вони кешували в процесі з короткими TTL і поверталися до MySQL, якщо потрібно. Redis знову став кешем, а не суддею.

Incident #2: the optimization that backfired

Інцидент №2: оптимізація, що повернулася бумерангом

Маркетплейс хотів пришвидшити читання профілів. Вони побудували write-through потік: коли користувач оновлював профіль, сервіс записував денормалізований blob профілю в Redis і потім оновлював MySQL. Читання були Redis-першими, без fallback на MySQL, окрім «в часи технічних робіт».

Все працювало чудово, доки деплой не вніс тонкий баг: ретраї при помилках запису в MySQL були неідемпотентними. Код додавав нові налаштування замість того, щоб замінювати їх, а запис у кеш відбувався до запису в MySQL. Під короткою подією блокування MySQL сервіс ретраїв. Redis тепер мав найновіший blob (з дубльованими налаштуваннями), тоді як MySQL мав суміш старих і частково оновлених рядків залежно від того, який ретрай пройшов.

Клієнти бачили неконсистентні профілі залежно від того, який інстанс сервісу їх обслуговував і який кеш-вузол вони попадали. Підтримка описувала це як «налаштування не зберігаються». Інженери — як «ми не знаємо, яка система права». Кеш став другою системою істини без операційної зрілості бази даних.

Ролбек відновив часткову адекватність, але справжній ремонт зайняв більше часу: вони перейшли на модель, де MySQL авторитетний, а Redis оновлюється через change stream після коміту. Також додали поле версії і відкидали старі записи в кеші. Оптимізація була швидшою. Вона також була брехливою.

Incident #3: the boring but correct practice that saved the day

Інцидент №3: нудна, але правильна практика, що врятувала день

Сервіс поруч з платіжною підсистемою (не основний леджер, але достатньо чутливий) використовував cache-aside з жорсткими правилами: кеші можуть бути застарілими, але ніколи не використовуватися для остаточних балансових рішень. Кожен ключ кешу мав TTL, версію і власника. Кожен виклик Redis мав короткий таймаут і стратегію fallback, задокументовану в рукбуці.

Одного дня failover Redis (через Sentinel) спричинив коротке вікно, де деякі клієнти писали в старий primary, а деякі — в новий. Це само по собі не було катастрофою; це той хаос, що слід очікувати. Їхній застосунок впорався, бо вони трактували Redis як best effort. Записи йшли в MySQL; інвалідизації кешу здійснювалися, але вони не були необхідні для коректності.

Система сповільнилася. Спрацювали алерти. Але сервіс залишився живим, і межа коректності залишилася неушкодженою. On-call виконав рукбук: тимчасово відключив читання кешу для найгарячіших ендпойнтів, дозволив MySQL обробляти читання деякий час і спостерігав стабілізацію помилок.

Ніяких героїчних вчинків. Жодних нових алгоритмів. Просто чіткі контракти і готовність прийняти тимчасове погіршення продуктивності заради незіпсованого стану. Така «нудна» дисципліна — те, що мають на увазі, кажучи, що надійність — це фіча.

Fast diagnosis playbook

Плейбук для швидкої діагностики

Коли все сповільнюється або з’являється неконсистентність, часу на філософію немає. Потрібна коротка послідовність, що визначає вузьке місце і домен відмови.

First: decide if it’s Redis-path, MySQL-path, or the app

Перший крок: визначте, чи проблема в шляху Redis, MySQL чи в застосунку

  1. Перевірте симптоми з боку користувача: читання повільні, записи повільні чи обидва? Помилки — таймаути чи невідповідність даних?
  2. Перевірте латентність і насиченість Redis: миттєві спайки латентності, заблоковані клієнти, вилучення ключів (evictions).
  3. Перевірте конкурентність MySQL: виконувані запити, очікування блокувань, затримка реплікації.
  4. Перевірте здоров’я пулу застосунку: потоки/пули з’єднань, глибина черг, паузи GC.

Second: test bypass paths

Другий крок: протестуйте шляхи обходу

  • Якщо безпечно обійти читання Redis для гарячої точки, зробіть це та подивіться, чи нормалізується латентність. Якщо так — винен шлях Redis (або бібліотека клієнта).
  • Якщо можна безпечно обійти репліки MySQL і тимчасово читати з primary, зробіть це, щоб перевірити затримку реплікації.

Third: validate correctness boundary

Третій крок: перевірте межу коректності

  • Візьміть одного користувача/об’єкт з відомим нещодавнім оновленням і порівняйте значення в MySQL та Redis напряму.
  • Якщо вони відрізняються, дізнайтеся, чи ваш патерн може таке породити (гонка інвалідизації vs частковий dual-write) і дійте відповідно.

Practical tasks: commands, outputs, and decisions

Практичні завдання: команди, виводи та рішення

Це не іграшкові команди. Це ті, які ви запускаєте о 2-й ночі, щоб припинити гадання. Кожне завдання включає, що означає вивід і яке рішення з нього випливає.

Task 1: Is Redis responding quickly from the app host?

Завдання 1: Redis відповідає швидко з хоста застосунку?

cr0x@server:~$ redis-cli -h redis-01 -p 6379 --latency -i 1
min: 0, max: 2, avg: 0.31 (1000 samples)
min: 0, max: 85, avg: 1.12 (1000 samples)

Значення: Другий рядок показує поодинокі спайки до 85 мс. Це не смертельно, але якщо таймаут застосунку 50 мс — ви отримаєте помилки.

Рішення: Якщо max/avg вище вашого SLO, дослідіть CPU Redis, налаштування persistence fsync, мережевий джиттер або повільні команди. Розгляньте тимчасове зниження залежності від кешу (fallback), якщо ви на write-through.

Task 2: Are Redis clients piling up?

Завдання 2: Чи накопичуються клієнти Redis?

cr0x@server:~$ redis-cli -h redis-01 INFO clients | egrep 'connected_clients|blocked_clients'
connected_clients:1248
blocked_clients:37

Значення: blocked_clients > 0 часто вказує на BLPOP/BRPOP споживачів, Lua скрипти або клієнтів, що чекають на подію, яка не настає. У кешах заблоковані клієнти зазвичай — погана ознака.

Рішення: Ідентифікуйте блокуючі команди, перевірте повільні Lua скрипти або застряглі транзакції. Якщо blocked_clients зростає разом з латентністю — вважайте це деградацією сервісу і знижуйте навантаження на кеш.

Task 3: Is Redis evicting keys (memory pressure)?

Завдання 3: Redis видаляє ключі (натиск пам’яті)?

cr0x@server:~$ redis-cli -h redis-01 INFO stats | egrep 'evicted_keys|keyspace_hits|keyspace_misses'
keyspace_hits:93811233
keyspace_misses:12100444
evicted_keys:482919

Значення: Евікшени означають, що ваш кеш перестає бути кешем, а стає машиною для блукання даних. Високі промахи ампліфікують навантаження на MySQL. Евікшени також руйнують будь-яке припущення про «гарячість» write-through.

Рішення: Збільшити maxmemory, підправити TTL, зменшити розміри значень, покращити розподіл ключів або змінити політику евікшн. У випадку інциденту: відключити кешування для низьковартісних ендпойнтів, щоб зменшити чурн.

Task 4: What eviction policy is configured?

Завдання 4: Яка політика евікшн налаштована?

cr0x@server:~$ redis-cli -h redis-01 CONFIG GET maxmemory-policy
1) "maxmemory-policy"
2) "allkeys-lru"

Значення: allkeys-lru видаляє будь-який ключ під тиском. Якщо ви зберігаєте сесії/локи поруч із кеш-записами, вони теж будуть евікнуті. Так ви отримуєте випадкові логаути і питання «чому завдання виконалося двічі?».

Рішення: Відокремте критичні ключі в інший інстанс Redis або використайте політику volatile-ttl для чистих кешів. Не змішуйте «неповинно зникати» дані з best-effort кешами.

Task 5: Are slow commands blocking Redis?

Завдання 5: Чи блокують Redis повільні команди?

cr0x@server:~$ redis-cli -h redis-01 SLOWLOG GET 5
1) 1) (integer) 912341
   2) (integer) 1766812230
   3) (integer) 58321
   4) 1) "ZRANGE"
      2) "leaderboard"
      3) "0"
      4) "50000"
      5) "WITHSCORES"
   5) "10.21.4.19:51722"
   6) ""

Значення: 58 мс ZRANGE, що повертає 50k елементів, заблокує цикл подій. Redis швидкий, але він не чудотворець. Великі відповіді дорогі.

Рішення: Обмежіть діапазони, пагінуйте або переробіть доступ до даних. Якщо це кеш, великі sorted set-и часто є випадковою продуктовою вимогою, схованою під технічною.

Task 6: Is Redis persistence causing write latency?

Завдання 6: Чи персистентність Redis викликає затримки запису?

cr0x@server:~$ redis-cli -h redis-01 INFO persistence | egrep 'aof_enabled|aof_last_write_status|rdb_bgsave_in_progress'
aof_enabled:1
aof_last_write_status:ok
rdb_bgsave_in_progress:0

Значення: AOF увімкнено. Якщо диск повільний або fsync агресивний, латентність запису може спайкати — особливо болісно для write-through.

Рішення: Для чистого кешу розгляньте відключення AOF або менш жорстку політику fsync. Для випадків, близьких до коректності, виміряйте дискову латентність і переконайтеся, що налаштування персистентності відповідають вашій моделі ризику.

Task 7: Is MySQL saturated or waiting on locks?

Завдання 7: MySQL насичений або чекає блокувань?

cr0x@server:~$ mysql -h mysql-01 -e "SHOW PROCESSLIST" | head
Id	User	Host	db	Command	Time	State	Info
4123	app	10.21.5.11:53312	prod	Query	12	Waiting for table metadata lock	UPDATE users SET ...
4188	app	10.21.5.18:50221	prod	Query	9	Sending data	SELECT ...

Значення: Очікування metadata lock вказує на DDL або зміни схеми, що конфліктують з трафіком. Cache-aside не врятує вас, якщо записи застрягли на блокуваннях.

Рішення: Зупиніть/відкотіть DDL або виконуйте його в неробочий час за допомогою online schema change інструментів. Тимчасово зменшіть конкуренцію записів або відключіть фічі, що б’ють по заблокованих таблицях.

Task 8: What does InnoDB say is happening right now?

Завдання 8: Що InnoDB каже про поточний стан?

cr0x@server:~$ mysql -h mysql-01 -e "SHOW ENGINE INNODB STATUS\G" | egrep -i 'LATEST DETECTED DEADLOCK|Mutex spin waits|history list length' | head -n 20
LATEST DETECTED DEADLOCK
Mutex spin waits 0, rounds 0, OS waits 0
History list length 987654

Значення: Велика довжина history list може вказувати на відставання purge через довгі транзакції, що веде до розростання і погіршення продуктивності. Deadlock-и можуть показувати патерни конкурентних записів.

Рішення: Визначте довготривалі транзакції, зменшіть область транзакцій в застосунку або відрегулюйте ізоляцію та індекси. Якщо інвалідації кешу залежать від цих записів — вони теж будуть накопичуватися.

Task 9: Is replication lag causing stale reads (blamed on cache)?

Завдання 9: Чи затримка реплікації викликає застарілі читання (звинувачують кеш)?

cr0x@server:~$ mysql -h mysql-replica-01 -e "SHOW REPLICA STATUS\G" | egrep 'Seconds_Behind_Source|Replica_IO_Running|Replica_SQL_Running'
Replica_IO_Running: Yes
Replica_SQL_Running: Yes
Seconds_Behind_Source: 47

Значення: 47 секунд відставання виглядатимуть саме як «застарілість кешу», якщо ваш додаток читає з реплік. Ви інвалідовуєте кеш і все одно читаєте старі дані.

Рішення: Маршрутуйте read-after-write трафік на primary (або використовуйте GTID-базовану консистентність читань). Виправте вузькі місця реплікації перш ніж переробляти логіку кешування.

Task 10: Are Redis keys expiring in a synchronized wave?

Завдання 10: Чи ключі Redis спливають синхронною хвилею?

cr0x@server:~$ redis-cli -h redis-01 --scan --pattern 'user:*' | head -n 5
user:10811
user:10812
user:10813
user:10814
user:10815

Значення: Ви пробуєте простір ключів. Якщо більшість ключів мають однакові TTL (ви перевірите наступним кроком), ви створюєте умови для stampede.

Рішення: Додайте TTL джиттер (випадкове зміщення) або реалізуйте раннє оновлення і single-flight блокування для гарячих ключів.

Task 11: Check TTL distribution for a hot key

Завдання 11: Перевірте розподіл TTL для гарячого ключа

cr0x@server:~$ redis-cli -h redis-01 TTL user:10811
(integer) 60

Значення: Чіткий TTL у 60 секунд підозрілий, якщо його широко застосовано. Багато застосунків роблять саме так і дивуються, чому база падає кожну хвилину.

Рішення: Змініть на щось на кшталт 60±15 секунд джиттер, або використайте soft TTL (подавати застаріле під час рефрешу).

Task 12: Validate a suspected inconsistency (Redis vs MySQL)

Завдання 12: Перевірте підозрювану неконсистентність (Redis vs MySQL)

cr0x@server:~$ redis-cli -h redis-01 GET user:10811
{"id":10811,"email":"old@example.com","version":17}
cr0x@server:~$ mysql -h mysql-01 -e "SELECT id,email,version FROM users WHERE id=10811"
id	email	version
10811	new@example.com	18

Значення: Redis відстає. При cache-aside це може бути гонка інвалідизації або пропущене видалення. При write-through це може бути часткова помилка подвійного запису або баг у порядку реплею.

Рішення: Якщо cache-aside: видаліть ключ і проведіть аудит шляхів інвалідизації, додайте версіоновані записи. Якщо write-through: трактуйте це як інцидент консистентності — призупиніть запис, знайдіть шлях невдалого dual-write, розгляньте перехід на оновлення кешу після коміту з DB-driven підходом.

Task 13: Check Redis replication health (if you run replicas)

Завдання 13: Перевірка здоров’я реплікації Redis (якщо у вас є репліки)

cr0x@server:~$ redis-cli -h redis-01 INFO replication | egrep 'role|connected_slaves|master_link_status|master_last_io_seconds_ago'
role:master
connected_slaves:1
master_link_status:up
master_last_io_seconds_ago:1

Значення: Якщо master_link_status — down або last_io високий, можлива відбій чи репліки відстають. При write-through з читаннями з реплік це пастка для консистентності.

Рішення: Віддавайте перевагу читанню з master для сильнішої консистентності, або прийміть eventual-consistency і трактуйте застарілі читання як очікувані. Не прикидайтеся, що маєте обидва.

Task 14: Check Linux-level network pain from app to Redis/MySQL

Завдання 14: Перевірте мережеві проблеми на рівні Linux між застосунком і Redis/MySQL

cr0x@server:~$ ss -tan state established '( dport = :6379 or dport = :3306 )' | wc -l
842

Значення: Велика кількість встановлених з’єднань може вказувати на відсутність пулінгу, оновлення з’єднань або застряглі сокети. Це перетворюється в латентність і потім таймаути.

Рішення: Переконайтеся, що застосунок використовує пул з’єднань, налаштуйте ліміти клієнтів, перевірте Redis maxclients / MySQL max_connections. В інцидентах обмежте конкурентність на вході.

Task 15: Check Redis memory and fragmentation

Завдання 15: Перевірте пам’ять і фрагментацію Redis

cr0x@server:~$ redis-cli -h redis-01 INFO memory | egrep 'used_memory_human|used_memory_rss_human|mem_fragmentation_ratio'
used_memory_human:12.31G
used_memory_rss_human:16.02G
mem_fragmentation_ratio:1.30

Значення: Коефіцієнт фрагментації 1.30 вказує на накладні витрати аллокатора/фрагментацію. Не завжди фатально, але зменшує ефективний розмір кешу і може викликати евікшени.

Рішення: Якщо ви обмежені евікшенами, розгляньте рестарт у вікні техобслуговування, увімкнення active defrag або зміну розміру пам’яті. Також зменшіть churn значень.

Common mistakes (symptom → root cause → fix)

Поширені помилки (симптом → причина → виправлення)

1) “Redis is up but the site is down”

1) «Redis працює, але сайт не відповідає»

Симптом: Висока латентність запитів і таймаути; Redis показує здоровий CPU і пам’ять.

Причина: Таймаути на боці клієнта занадто великі + ретраї + вичерпання thread pool. Redis «up», але ваш застосунок застряг у очікуванні.

Виправлення: Встановіть агресивні таймаути (зазвичай 5–50 мс залежно від топології), обмежте ретраї, додайте circuit breaker, забезпечте fallback на MySQL для cache-aside читань.

2) “We invalidated the cache and it’s still stale”

2) «Ми інвалідовували кеш, а він все ще застарілий»

Симптом: Після оновлення деякі користувачі бачать старі дані хвилинами.

Причина: Гонка інвалідизації (видалення потім повторна заповнення старим значенням), або відставання реплікації (ви читаєте старі дані з репліки і репопулюєте кеш).

Виправлення: Використовуйте версіоновані ключі або CAS-записи; маршрутуйте read-after-write на primary або забезпечте консистентність читань; додайте TTL джиттер і single-flight.

3) “Writes got slower after we added caching”

3) «Записи уповільнилися після додавання кешу»

Симптом: P95 латентності записів зріс; з’явилися таймаути на endpoint-ах оновлення.

Причина: Write-through додав Redis у критичний шлях; persistence fsync або мережевий джиттер ампліфікує латентність.

Виправлення: Робіть запис у кеш асинхронно (DB-authoritative + CDC), або прийміть cache-aside з інвалідизацією; налаштуйте персистентність Redis або відокремте Redis для кешу.

4) “Random logouts, duplicate jobs, missing locks”

4) «Випадкові логаути, дубльовані завдання, пропущені локи»

Симптом: Сесії зникають; бекграунд-завдання виконуються двічі; розподілені локи ламаються.

Причина: Використання одного Redis для волатильних кеш-ключів і критичних координаційних ключів; політика евікшн видаляє важливі ключі.

Виправлення: Відокремте інстанси Redis або принаймні бюджети пам’яті/політики; уникайте евікшн-у для координаційних даних; моніторьте evicted_keys.

5) “Every minute the database melts”

5) «Щохвилини база даних плавиться»

Симптом: Періодичні спайки QPS і латентності MySQL, вирівняні з TTL-границями.

Причина: Синхронізоване спливання TTL по гарячих ключах; stampede.

Виправлення: TTL джиттер, раннє оновлення, request coalescing, serve-stale-while-revalidate і часткове кешування дорогих компонентів.

6) “Cache hit rate is high but performance is still bad”

6) «Хітрейт кешу високий, але продуктивність все ще погана»

Симптом: Статистика hit rate Redis виглядає чудово; застосунок все одно повільний.

Причина: Залишкові промахи можуть бути найвитратнішими, або виклики Redis повільні/великі, або витрачається CPU на серіалізацію/десеріалізацію; застосунок CPU-bound.

Виправлення: Виміряйте розмір payload, компресуйте вибірково, зберігайте менші проекції, уникайте важких range-запитів, профілюйте CPU застосунку.

7) “We have data corruption but no errors”

7) «У нас корупція даних, але без помилок»

Симптом: Користувачі бачать суперечливий стан залежно від ендпойнту.

Причина: Подвійний запис без идемпотентності і без гарантій порядку; дизайн write-through припускав симетрію успіху.

Виправлення: Припиніть dual-write у шляхах запитів. Використовуйте transactional outbox/CDC для оновлення кешу після коміту; додайте перевірки версій; визначте одне джерело істини.

Checklists / step-by-step plan

Чеклісти / покроковий план

Pick the pattern: a practical decision tree

Вибір патерну: практичне дерево рішень

  1. Чи може система працювати коректно без Redis?
    • Так → за замовчуванням обирайте cache-aside.
    • Ні → ви будуєте розподілене сховище. Трактуйте Redis як критичну інфраструктуру і подумайте, чи MySQL все ще потрібен у гарячому шляху.
  2. Чи потрібна вам read-after-write консистентність для користувацьких потоків?
    • Так → cache-aside з читанням із primary для сесії, або DB-driven оновлення кешу з версіонуванням.
    • Ні → cache-aside з TTL + джиттер зазвичай підходить.
  3. Чи часті та чутливі до латентності записи?
    • Так → уникайте синхронного write-through, якщо тільки Redis не дуже близький і добре опротокольований.
    • Ні → write-through може бути прийнятним, якщо він спрощує читання і ви можете забезпечити идемпотентність.

Cache-aside runbook: “correctness first” implementation steps

Рукбук для cache-aside: «спочатку коректність» — кроки імплементації

  1. Визначте джерело істини: MySQL є авторитетним. Redis — похідний.
  2. Шлях читання: Redis GET → при промасі читати MySQL → встановити Redis з TTL + джиттер.
  3. Шлях запису: Записати MySQL в транзакції → після коміту інвалідовувати (DEL) або оновити Redis.
  4. Запобігання stampede: реалізуйте single-flight на ключ (mutex з коротким TTL) або подавайте застаріле під час рефрешу.
  5. Додайте версіонування: вбудовуйте версію у значення; відхиляйте старі записи в Redis, якщо можете.
  6. Таймаутіть швидко: короткий таймаут для Redis; якщо він спрацьовує — пропускайте кеш і читайте MySQL.
  7. Спостерігайте: відстежуйте hit rate, evictions, латентність і QPS MySQL під час тестів обходу кешу.

Write-through runbook: if you insist, do it like you mean it

Рукбук для write-through: якщо наполягаєте — робіть це серйозно

  1. Зробіть записи идемпотентними: ретраї не повинні створювати новий стан. Використовуйте request IDs, версії або upsert-операції з обережністю.
  2. Визначте порядок: last-write-wins потребує таймстемпу/версії, що монотонно зростає на об’єкт.
  3. Плануйте поведінку при часткових відмовах: якщо запис у Redis не вдався, а MySQL успішний — що відбувається? Якщо MySQL впав, а Redis успішно записався — як це виправляти?
  4. Віддавайте перевагу DB-driven оновленням кешу: коміт у MySQL, потім оновлення Redis асинхронним воркером через outbox/CDC.
  5. Бюджетуйте латентність: Redis стає частиною write SLO. Міряйте, ставте алерти й плануйте ємність відповідно.
  6. Ізоляція: не діліть цей Redis із best-effort кешами та випадковими експериментами фіч.

Incident checklist: keep the service alive

Чекліст інциденту: тримайте сервіс живим

  1. Зменшіть радіус ураження: відключіть читання кешу для найгарячіших ендпойнтів, якщо це безпечно.
  2. Обмежте конкурентність на вході (shed load краще, ніж повний колапс).
  3. Перевірте евікшени та латентність Redis; перевірте блокування MySQL і затримку реплікації.
  4. Якщо коректність скомпрометована (dual-write неконсистентність), заморозьте записи або маршрутуйте читання на MySQL, поки не відновите.
  5. Після стабілізації — відновіть кеш і проведіть семплінг на консистентність.

FAQ

Питання й відповіді

1) Which breaks less: cache-aside or write-through?

1) Що ламається рідше: cache-aside чи write-through?

У більшості продуктових застосунків: cache-aside ламається рідше, бо може деградувати до MySQL, коли Redis повільний або недоступний. Write-through робить Redis частиною доступності записів.

2) Can write-through be made safe?

2) Чи можна зробити write-through безпечним?

Так, але «безпечний» зазвичай означає не чисто синхронний dual-write. Безпечніша модель — це коміт у MySQL спочатку, а потім асинхронне оновлення кешу через outbox/CDC з версіонуванням.

3) Why not just rely on Redis persistence and skip MySQL?

3) Чому просто не покладатися на персистентність Redis і не відмовитися від MySQL?

Іноді це виправдано, але це інша архітектура. Персистентність і шардування Redis можуть працювати, але ви втрачаєте реляційні запити і отримуєте нові операційні обмеження. Не заходьте у це неусвідомлено.

4) What TTL should I use?

4) Який TTL варто використовувати?

Обирайте TTL, виходячи з того, наскільки болісна застарілість і наскільки дорогий промах. Потім додайте джиттер (випадкове зміщення), щоб уникнути синхронного спливання. Гарячі ключі часто потребують окремої логіки крім простого TTL.

5) Should I update cache on write or delete/invalidate?

5) Оновлювати кеш при записі чи видаляти/інвалідовувати?

Інвалідизація простіша і часто безпечніша, але може збільшити промахи. Оновлення при записі зменшує промахи, але ускладнює правильність. Якщо ви оновлюєте при записі, використайте версіонування або CAS-семантику, щоб уникнути гонок.

6) How do I prevent cache stampede?

6) Як запобігти stampede кешу?

Використовуйте single-flight (лок на ключ), подавайте застаріле під час рефрешу, раннє оновлення та TTL джиттер. Також розгляньте negative caching для неіснуючих елементів.

7) Why is my hit rate high but MySQL still hot?

7) Чому хітрейт високий, але MySQL все ще гарячий?

Бо залишкові промахи можуть бути найдорожчими, або виклики Redis повільні/великі, або ви робите додаткову роботу в MySQL на запит (join-и, блокування, другорядні запити), не пов’язані з кешованими об’єктами.

8) Is Redis Cluster required for caching?

8) Чи потрібен Redis Cluster для кешування?

Ні. Багато кешів добре працюють з primary+replica і Sentinel для failover, або навіть з одним інстансом, якщо ви терпите втрату. Cluster додає операційний оверхед і обмеження по hash slot — він виправданий, коли потрібен горизонтальний масштаб.

9) How do I debug “stale data” complaints fast?

9) Як швидко дебажити скарги на «застарілі дані»?

Виберіть один об’єкт, порівняйте Redis і MySQL значення напряму і перевірте затримку реплікації. Далі визначте, чи це гонка інвалідизації (cache-aside) або частковий dual-write (write-through).

Conclusion: next steps you can ship

Висновок: наступні кроки, які можна відправити в продакшн

Якщо ви хочете того, що ламається рідше, оберіть cache-aside з короткими таймаутами, розумними fallback-ами і захистом від stampede. Трактуйте Redis як шар продуктивності, а не шар істини. Коли Redis падає, ви маєте отримати повільніший сервіс, а не неправільний.

Якщо вам справді потрібна семантика write-through, не робіть наївні подвійні записи в шляхах запитів. Зробіть MySQL авторитетним, оновлюйте Redis після коміту і додайте версіонування, щоб старі записи кешу не могли воскресити застарілий стан.

Конкретні наступні кроки:

  1. Аудит всіх викликів Redis: таймаут, політика ретраїв і чи може запит пройти без Redis.
  2. Додайте дашборди/алерти для латентності Redis, evictions, blocked clients і затримки реплікації MySQL.
  3. Реалізуйте TTL джиттер і single-flight для ваших топ-20 ключів за QPS.
  4. Проведіть game day: відключіть читання Redis для одного ендпойнту і перевірте, чи витримає MySQL навантаження достатньо довго для реагування на інцидент.
  5. Запишіть межу коректності простою англійською (у вашому випадку — українською) і примусьте її дотримуватися в code review.

Продакшн-системи не винагороджують винахідливість. Вони винагороджують контракти, що тримаються під стресом.

← Попередня
Як читати огляди GPU: пастки 1080p, 1440p і 4K
Наступна →
MariaDB проти PostgreSQL на VPS: налаштування для максимальної швидкості за гроші

Залишити коментар