Модуль 17

Lua-скрипти в Redis

Ось готовий урок, створений за твоїм майстер-промптом. Це не просто лекція, це — інтерактивна подорож у світ атомарності та швидкості 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 не обробляє жодних інших команд від інших клієнтів. Світ завмирає, поки ваш скрипт працює.

Що потрібно знати залізно:

  1. Мова Lua: Це дуже проста, легка мова скриптів. Вам не треба бути експертом у Lua, достатньо знати базовий синтаксис (if/else, змінні).
  2. Команда EVAL: Це команда, якою ми передаємо скрипт у Redis.
  3. redis.call(): Функція всередині Lua, яка дозволяє виконувати звичайні команди Redis (SET, GET, INCR тощо).
  4. 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?

❌ Типові помилки новачка:

  1. "Зашивання" ключів: Пише redis.call('GET', 'user:1') прямо в скрипті замість використання KEYS[1].
    • Чому це погано: Це ламає кешування скриптів і роботу в кластері.
  2. Важкі обчислення: Робить цикл на 1 мільйон ітерацій всередині скрипта.
    • Чому це погано: Пам’ятаєте? Redis однопотоковий. Поки ваш цикл крутиться, весь ваш сайт лежить для всіх користувачів. Скрипти мають бути миттєвими.
  3. Індексація Lua: Забуває, що в Lua масиви починаються з 1, а не з 0. Це класичні граблі.

✅ Як думає Senior Developer:

  1. Оптимізація трафіку: "Навіщо я ганяю цей текст скрипта щоразу через мережу? Я використаю SCRIPT LOAD один раз, отримаю SHA-хеш, і буду викликати EVALSHA." Це економить трафік.
  2. Атомарність — це інструмент: "Тут мені треба гарантія транзакції. Lua — ідеально. А тут просто треба прочитати дані? Lua не потрібен, зроблю звичайний GET".
  3. Логіка на боці даних: "Замість того щоб тягнути дані до коду, обробляти й слати назад, я відправлю логіку (скрипт) до даних".

6. 🧩 Підсумок

Отже, що ми сьогодні розібрали?

  1. Lua в Redis — це ваш спосіб створювати власні, атомарні команди.
  2. Це вирішує проблему Race Conditions (як у прикладі з квитками).
  3. Це прискорює роботу, зменшуючи кількість запитів до сервера (як у прикладі з Rate Limiter).
  4. Пам’ятайте про головне правило: скрипти мають бути швидкими, інакше ви зупините весь світ.

Тепер ви не просто "користувач Redis", ви вмієте розширювати його можливості.

👉 Тизер наступного уроку: Окей, ми навчилися працювати з даними в пам'яті швидко і безпечно. Але що станеться, якщо хтось висмикне шнур із розетки? У наступному уроці ми поговоримо про Redis Persistence (RDB та AOF) — як не втратити все, що ми так старанно зберігали.


Маєш питання чи хочеш розібрати рішення задач? Кажи, я тут!