Ось готовий урок, створений за твоїм майстер-промптом. Це не просто лекція, це — інтерактивна подорож у світ атомарності та швидкості Redis.
🎓 Урок: Lua-скрипти в Redis
(Або: Як змусити Redis робити саме те, що ти хочеш, і нікого не слухати)
1. 🔥 Вступ: Коли все йде шкереберть
Уявіть собі таку ситуацію. Ви пишете бекенд для інтернет-магазину, де продаються квитки на концерт улюбленого гурту. Залишився останній квиток.
У цей самий момент двоє користувачів — Андрій та Олена — одночасно натискають кнопку "Купити".
Ваш код робить приблизно таке: 1. GET ticket_count (Redis відповідає: "Є 1 квиток"). 2. Ваш код думає: "О, супер, продаємо!". 3. SET ticket_count 0 (Оновлюємо базу).
Проблема: Оскільки Андрій та Олена натиснули кнопку одночасно, обидва їхні запити прочитали, що квиток є (крок 1). І обидва "купили" його. Тепер у вас продано 2 квитки, а місце в залі лише одне. Це називається Race Condition (стан гонитви).
Ви можете запитати: "А хіба ми не можемо просто сказати Redis: перевір і запиши одночасно?"
Стандартними командами — ні. Між командою GET і командою SET проходить час. Мікросекунди, але цього достатньо, щоб інший запит вклинився між ними.
Як це вирішити? Нам потрібен спосіб відправити на сервер пачку команд і сказати: "Виконай це все як одне ціле. Нікого не пускай, поки я не закінчу".
Саме тут на сцену виходять Lua-скрипти.
Аналогія: Це як у кав’ярні. * Без Lua: Ви питаєте баристу: "Є молоко?" (Він: Так). Поки ви шукаєте гаманець, хтось інший кричить "Мені лате!" і забирає останнє молоко. Ви платите, а молока вже немає. * З Lua: Ви даєте баристі записку: "Якщо є молоко — зроби мені лате і візьми гроші". Бариста читає записку і робить все одразу. Ніхто не може втрутитися, поки він читає вашу записку.
2. 🧠 Теоретична база (Як це працює під капотом)
Redis — це однопотокова система. Це означає, що він робить одну справу за раз.
Коли ви запускаєте Lua-скрипт у Redis, відбувається магія атомарності. Атомарність означає неподільність. Скрипт виконується від початку до кінця, і в цей час Redis не обробляє жодних інших команд від інших клієнтів. Світ завмирає, поки ваш скрипт працює.
Що потрібно знати залізно:
- Мова Lua: Це дуже проста, легка мова скриптів. Вам не треба бути експертом у Lua, достатньо знати базовий синтаксис (if/else, змінні).
- Команда
EVAL: Це команда, якою ми передаємо скрипт у Redis. redis.call(): Функція всередині Lua, яка дозволяє виконувати звичайні команди Redis (SET, GET, INCR тощо).- KEYS та ARGV: Це спосіб передачі даних у скрипт.
KEYS— назви ключів (наприклад,user:100:balance).ARGV— значення (наприклад, сума поповнення50).
Чому це важливо розділяти? Redis Cluster використовує
KEYS, щоб знати, на якому сервері виконувати скрипт. Якщо ви "зашиєте" ключ прямо в тіло скрипта, Redis може розгубитися.
Інтуїтивне розуміння: Lua-скрипт — це ваша власна, кастомна команда Redis, яку ви пишете на льоту.
3. 🧪 Приклади (Від "Привіт" до реальності)
Приклад 1: "Hello World"
Давайте просто змусимо Redis повернути нам те, що ми йому надіслали.
EVAL "return 'Привіт, ' .. KEYS[1] .. ' ' .. ARGV[1]" 1 Redis Студенте
Розбір:
* EVAL — "Запусти цей скрипт".
* "..." — сам код на Lua (.. це конкатенація, тобто з'єднання рядків).
* 1 — кількість ключів (ми передаємо один ключ).
* Redis — це KEYS[1].
* Студенте — це ARGV[1].
Результат: "Привіт, Redis Студенте"
Приклад 2: Безпечне зняття коштів (Check-and-Set)
Повернемося до проблеми. Ми хочемо зняти гроші з рахунку, тільки якщо їх достатньо.
Сценарій: У користувача user:1 є 100 монет. Ми хочемо зняти 110. Стандартний Redis дозволить піти в мінус, якщо ми просто зробимо DECRBY.
Рішення на Lua:
-- Цей код ми передаємо в EVAL
local current_balance = tonumber(redis.call('GET', KEYS[1]))
if current_balance >= tonumber(ARGV[1]) then
redis.call('DECRBY', KEYS[1], ARGV[1])
return "Успіх! Залишок: " .. (current_balance - ARGV[1])
else
return "Помилка: Недостатньо коштів"
end
Як це запустити (у CLI):
EVAL "local bal = tonumber(redis.call('GET', KEYS[1])); if bal >= tonumber(ARGV[1]) then redis.call('DECRBY', KEYS[1], ARGV[1]); return 1; else return 0; end" 1 user:1:balance 50
Питання до вас: Чому ми використовуємо
tonumber()?Відповідь: Тому що Redis зберігає значення як рядки. Lua не вміє математично порівнювати рядок "100" і число 50 без явного перетворення.
Приклад 3: Rate Limiter (Обмеження запитів)
Дуже популярна задача. "Дозволити користувачу робити не більше 5 запитів за 10 секунд".
Якщо робити це з коду програми: 1. Отримати лічильник. 2. Перевірити, чи він < 5. 3. Збільшити. 4. Якщо це перший запит — встановити час життя (TTL).
Це 3-4 походи до сервера (Network Round Trips). Це повільно. Lua робить це за один захід.
local count = redis.call('INCR', KEYS[1])
if count == 1 then
-- Якщо це перший запит, ставимо таймер на 10 секунд
redis.call('EXPIRE', KEYS[1], ARGV[1])
end
return count
Ви викликаєте це, і Redis повертає вам поточну кількість запитів. Швидко, атомарно, елегантно.
4. 🛠 Практична частина
Прийшов час забруднити руки кодом. Якщо у вас встановлено redis-cli, робіть це там. Якщо ні — уявіть логіку.
Завдання 1: Розігрів
Створіть ключ mykey зі значенням 10. Напишіть Lua-скрипт, який повертає значення ключа, помножене на аргумент, який ви передасте.
Підказка: EVAL "return ... * ..." 1 mykey 5
Завдання 2: "Тільки парні"
Напишіть скрипт, який робить INCR (збільшує на 1) для ключа counter, АЛЕ тільки якщо поточне значення — парне число. Якщо непарне — нічого не робить і повертає -1.
Підказка: Оператор залишку від ділення в Lua — це %. 4 % 2 == 0.
Завдання 3: Виправ помилку (Debug)
Студент написав скрипт для додавання елемента в список, тільки якщо його там ще немає (унікальний список).
-- Помилковий код
local items = redis.call('LRANGE', KEYS[1], 0, -1)
for i, item in ipairs(items) do
if item == ARGV[1] then
return "Вже є!"
end
end
redis.call('RPUSH', KEYS[0], ARGV[1]) -- <--- ДЕ ТУТ ПОМИЛКА?
return "Додано"
Знайдіть помилку в передостанньому рядку.
Завдання 4: Міні-кейс "Спільний кошик"
Уявіть, що ви робите спільний список покупок.
Напишіть скрипт, який приймає назву товару (ARGV[1]).
1. Перевіряє, чи є вже такий товар у Set-і (SISMEMBER).
2. Якщо є — повертає "Вже купили".
3. Якщо немає — додає його (SADD) і повертає "Додано".
5. 💡 Мислення як у розробника
Як думає новачок, а як — профі, працюючи з Lua в Redis?
❌ Типові помилки новачка:
- "Зашивання" ключів: Пише
redis.call('GET', 'user:1')прямо в скрипті замість використанняKEYS[1].- Чому це погано: Це ламає кешування скриптів і роботу в кластері.
- Важкі обчислення: Робить цикл на 1 мільйон ітерацій всередині скрипта.
- Чому це погано: Пам’ятаєте? Redis однопотоковий. Поки ваш цикл крутиться, весь ваш сайт лежить для всіх користувачів. Скрипти мають бути миттєвими.
- Індексація Lua: Забуває, що в Lua масиви починаються з 1, а не з 0. Це класичні граблі.
✅ Як думає Senior Developer:
- Оптимізація трафіку: "Навіщо я ганяю цей текст скрипта щоразу через мережу? Я використаю
SCRIPT LOADодин раз, отримаю SHA-хеш, і буду викликатиEVALSHA." Це економить трафік. - Атомарність — це інструмент: "Тут мені треба гарантія транзакції. Lua — ідеально. А тут просто треба прочитати дані? Lua не потрібен, зроблю звичайний GET".
- Логіка на боці даних: "Замість того щоб тягнути дані до коду, обробляти й слати назад, я відправлю логіку (скрипт) до даних".
6. 🧩 Підсумок
Отже, що ми сьогодні розібрали?
- Lua в Redis — це ваш спосіб створювати власні, атомарні команди.
- Це вирішує проблему Race Conditions (як у прикладі з квитками).
- Це прискорює роботу, зменшуючи кількість запитів до сервера (як у прикладі з Rate Limiter).
- Пам’ятайте про головне правило: скрипти мають бути швидкими, інакше ви зупините весь світ.
Тепер ви не просто "користувач Redis", ви вмієте розширювати його можливості.
👉 Тизер наступного уроку: Окей, ми навчилися працювати з даними в пам'яті швидко і безпечно. Але що станеться, якщо хтось висмикне шнур із розетки? У наступному уроці ми поговоримо про Redis Persistence (RDB та AOF) — як не втратити все, що ми так старанно зберігали.
Маєш питання чи хочеш розібрати рішення задач? Кажи, я тут!