Ось готовий урок, створений у стилі CS50, спеціально для тебе. Уяви, що ми зараз в аудиторії Сандерс-театру в Гарварді (або просто в Zoom з кавою), і розбираємо цю тему драйвово та по суті.
🎓 CS50: Distributed Locks з Redis
Привіт, світе! 👋 Радий вас бачити.
Сьогодні ми торкнемося теми, яка відділяє "просто код, що працює на моєму ноутбуці" від "систем, що тримають навантаження Чорної п’ятниці".
Ми говоритимемо про Distributed Locks (розподілені блокування) і про те, як Redis стає нашим рятівником у хаосі паралельних процесів.
1. 🔥 Вступ: Хаос Чорної п’ятниці
Уявіть ситуацію. Ви будуєте інтернет-магазин. На складі залишився один-єдиний iPhone 15 Pro Max за суперціною.
У цю саму секунду: * Андрій у Києві натискає кнопку "Купити". * Олена у Львові натискає кнопку "Купити".
Ваш код на сервері виглядає приблизно так:
1. Перевір, чи є товар (так, qty = 1).
2. Якщо є — оформи замовлення.
3. Зменш кількість товару (qty = 0).
Питання: Якщо запити Андрія та Олени прийдуть на сервер одночасно (або на два різних сервери вашого кластера), що станеться?
Обидва процеси перевірять наявність: "О, є 1 телефон!". Обидва оформлять замовлення. Обидва спишуть товар. У результаті: ви продали два телефони, а на складі був один.
Це називається Race Condition (стан гонитви). У розподілених системах, де у вас працює 10, 20 або 100 серверів одночасно, ви не можете просто використати змінну в пам’яті (mutex), бо у кожного сервера пам’ять своя.
Нам потрібен зовнішній арбітр. Хтось швидкий, надійний і такий, до якого мають доступ усі сервери. І цей арбітр — Redis.
Уявіть Redis як єдину вбиральню в літаку. Неважливо, скільки пасажирів (серверів) хочуть туди потрапити — якщо двері зачинені (Lock), решта мусить чекати.
2. 🧠 Теоретична база: Як це працює "під капотом"
Перш ніж писати код, зрозуміймо логіку. Redis працює переважно в один потік (single-threaded). Це означає, що всі команди виконуються по черзі. Це наша суперсила. Ніякі дві команди не можуть виконатися абсолютно одночасно.
Ключові поняття:
- Lock (Замок): Це просто запис у Redis (ключ-значення). Якщо запис існує — ресурс зайнятий.
- TTL (Time To Live): Час життя ключа. Це критично! Якщо ваш сервер "впаде" після того, як поставив замок, але перед тим, як його зняти — без TTL система заблокується навічно (Deadlock).
- Atomicity (Атомарність): Перевірка наявності замка і його встановлення мають відбуватися за один крок.
Що треба запам’ятати залізно:
Не можна робити так: "Перевір GET, якщо пусто — зроби SET". Між GET і SET може вклинитися інший процес!
Ми будемо використовувати команду, яка робить це атомарно. Раніше це була команда SETNX (SET if Not eXists), зараз це розширений SET з параметрами.
3. 🧪 Приклади: Від наївного до правильного
Для прикладів використаємо Python (псевдокод, зрозумілий кожному), але логіка ідентична для Java, Go чи JS.
❌ Спроба №1: Наївний підхід (Не робіть так!)
# Уявіть, що це функція покупки
def buy_item(item_id):
if redis.get(f"lock:{item_id}"):
return "Чекайте, товар обробляється іншим!"
# <--- ТУТ МОЖЕ ВКЛИНИТИСЯ ІНШИЙ ПРОЦЕС!
redis.set(f"lock:{item_id}", "locked")
# ... логіка покупки ...
redis.delete(f"lock:{item_id}")
Чому це погано: Це класичний Race Condition. Двоє можуть пройти перевірку if одночасно.
✅ Спроба №2: Атомарність (Вже краще)
Використовуємо аргумент nx=True (Only set if Not Exists).
def buy_item_safe(item_id):
# Намагаємося встановити замок.
# Redis поверне True, ТІЛЬКИ якщо ключа ще не було.
is_locked = redis.set(f"lock:{item_id}", "locked", nx=True)
if not is_locked:
return "Зайнято, спробуйте пізніше!"
try:
# ... логіка покупки ...
pass
finally:
redis.delete(f"lock:{item_id}")
Що тут не так? Уявіть, що під час виконання логіки покупки світло вимкнули, і сервер згорів. Код не дійшов до finally. Ключ lock:item_id залишився в Redis назавжди. Товар більше ніхто ніколи не купить.
🚀 Спроба №3: Production Ready (З TTL та Унікальністю)
import uuid
def buy_item_pro(item_id):
my_unique_id = str(uuid.uuid4()) # Унікальний підпис цього процесу
# 1. Атомарно ставимо замок + авто-видалення через 10 секунд (ex=10)
is_locked = redis.set(f"lock:{item_id}", my_unique_id, nx=True, ex=10)
if not is_locked:
return "Ресурс зайнятий"
try:
# ... критична секція (купівля товару) ...
# Важливо: вона має виконуватися швидше, ніж 10 секунд!
print("Купуємо товар...")
finally:
# 2. Безпечне видалення
# Видаляємо замок ТІЛЬКИ якщо там записаний НАШ ID.
# (Це робиться Lua-скриптом для атомарності, але логіка така):
if redis.get(f"lock:{item_id}") == my_unique_id:
redis.delete(f"lock:{item_id}")
Чому ми перевіряємо ID перед видаленням?
Це захист від ситуації, коли ваш процес "завис" на 15 секунд.
1. Ваш замок (на 10 сек) протух і зник сам.
2. Інший процес прийшов і поставив свій замок.
3. Ваш процес "прокинувся" і виконав redis.delete.
Без перевірки ID ви видалите чужий замок! 😱
4. 🛠 Практична частина
Час розім’яти пальці. Відкрийте термінал або уявіть це.
Завдання 1: "Швидкий палець" Створіть скрипт, який у циклі 100 разів намагається захопити лок. Запустіть цей скрипт у двох різних терміналах одночасно. Порахуйте, скільки разів кожному вдалося захопити ресурс. Мета: Переконатися, що сума успішних захоплень ніколи не перевищує 100 (або логічного ліміту), і немає перетинів.
Завдання 2: "Симуляція краху"
Напишіть код, який ставить лок з TTL 5 секунд, але потім "засинає" (sleep) на 20 секунд.
Спробуйте запустити другий скрипт, поки перший спить.
Питання: Через скільки секунд другий скрипт зможе отримати доступ? (Має бути через 5, а не 20).
Завдання 3: "Чужий замок"
Спробуйте реалізувати помилку, про яку я говорив у прикладі №3.
1. Процес А ставить лок (TTL 2 сек), спить 5 сек, потім видаляє лок.
2. Процес Б стартує через 3 сек після А.
Спостереження: Перевірте логи Redis (MONITOR), чи видалив Процес А замок, який вже належав Процесу Б?
Завдання 4: Реальний кейс У вас є Cron-job (завдання за розкладом), який відправляє e-mail розсилку щоранку о 9:00. Ваш бекенд запущено на 4 серверах. Як використати Redis Lock, щоб користувачі не отримали 4 однакові листи?
5. 💡 Мислення як у розробника
Ви тепер знаєте механіку. Але як думає сеньйор?
- "Чи справді мені це треба?" Блокування сповільнюють систему. Якщо можна спроєктувати базу даних так, щоб вона сама рулила конфліктами (наприклад, Optimistic Locking в SQL), іноді це краще.
- Час життя (TTL) — це мистецтво. Занадто мало — замок зникне, поки ви ще працюєте (небезпечно). Занадто багато — якщо сервер впаде, ресурс буде недоступним довго. Порада: Ставте із запасом, але моніторте виконання.
- Бібліотеки. В реальному житті ми рідко пишемо сирі команди
SET NX. В Python ми візьмемоredis-pylock, в Java —Redisson. Вони реалізують алгоритм Redlock (надійніша версія для кластера Redis), автоматичне подовження життя замка (watchdog) та інші фішки.
Типова помилка новачка: Думати, що Redis Lock — це 100% гарантія. У дуже складних розподілених системах (де годинники на серверах можуть розходитися) навіть це може дати збій (детальніше читайте "Martin Kleppmann on Redlock"). Але для 99% задач — це саме те, що треба.
6. 🧩 Підсумок
Отже, що ми сьогодні поклали в нашу "валізу знань":
- Ми зрозуміли, що Distributed Lock — це спосіб координації серверів, щоб вони не билися за один ресурс.
- Ми дізналися, що команда
SET resource_name token NX PX time— це наш найкращий друг. - Ми навчилися, що видаляти замок треба обережно, перевіряючи, чи він все ще наш.
Тепер ви вмієте: Запобігати продажу неіснуючих товарів і подвійним списанням грошей. Ви зробили інтернет трохи безпечнішим і стабільнішим місцем.
Що далі? А що, якщо навантаження таке велике, що один Redis не справляється? Або сам Redis "впав"? Наступного разу ми поговоримо про Redis Sentinel, Cluster та те, як масштабувати пам'ять.
А поки що — це був CS50. До зустрічі! 👨💻