Bạn có biết?
Khi bạn cần thực hiện một logic phức tạp trong Redis — ví dụ: “giảm stock chỉ khi còn hàng, đồng thời tăng sold và ghi log” — các lệnh thông thường không đảm bảo atomicity giữa nhiều bước. Đây là lúc Lua Scripts phát huy sức mạnh, cho phép chạy toàn bộ logic như một lệnh duy nhất.
Lua Scripts là gì?
Redis tích hợp sẵn một Lua interpreter, cho phép bạn viết script bằng ngôn ngữ Lua và thực thi trực tiếp trên server. Toàn bộ script chạy như một atomic operation — không có lệnh nào khác xen vào giữa chừng.
So với MULTI/EXEC transactions:
- ✅ True atomicity — Không ai có thể đọc dữ liệu giữa chừng
- ✅ Conditional logic — if/else, loops, biến tạm
- ✅ Ít round-trips — Gửi 1 request thay vì nhiều
- ✅ Reusable — Script được cache trên server
Cú pháp cơ bản
EVAL — Thực thi script trực tiếp
# EVAL script numkeys key [key ...] arg [arg ...]
EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 mykey "myvalue"
# OK
# Giải thích:
# - "return redis.call('SET', KEYS[1], ARGV[1])": script Lua
# - 1: số lượng keys (KEYS[1])
# - mykey: giá trị của KEYS[1]
# - "myvalue": giá trị của ARGV[1]
KEYS và ARGV
# KEYS[]: Danh sách keys (truyền vào numkeys đầu tiên)
# ARGV[]: Danh sách arguments (sau keys)
# Ví dụ: 2 keys, 1 arg
EVAL "
local val1 = redis.call('GET', KEYS[1])
local val2 = redis.call('GET', KEYS[2])
return val1 .. ':' .. val2 .. ':' .. ARGV[1]
" 2 key1 key2 "suffix"
# "value1:value2:suffix"
redis.call vs redis.pcall
# redis.call: Ném lỗi nếu có lỗi (script dừng lại)
EVAL "return redis.call('GET', 'nonexistent:key')" 0
# (nil)
# redis.pcall: Bắt lỗi và trả về error object
EVAL "return redis.pcall('INCR', 'mykey')" 0
# Nếu mykey không phải số: {err="..."}
EVALSHA — Tối ưu băng thông
Mỗi lần dùng EVAL, Redis phải nhận toàn bộ nội dung script qua mạng. Nếu script dài và được gọi hàng nghìn lần mỗi giây, lượng bandwidth tiêu tốn rất lớn. EVALSHA giải quyết vấn đề này bằng cách chỉ gửi SHA1 hash (40 ký tự) thay vì toàn bộ script.
Flow hoạt động:
- 🔹 Bước 1:
SCRIPT LOAD "script_content"→ Redis trả về SHA1 hash và cache script trên server - 🔹 Bước 2:
EVALSHA <sha1_hash> numkeys ...→ Chỉ gửi 40 ký tự hash, tiết kiệm băng thông đáng kể - 🔹 Nếu script chưa cache: Redis trả lỗi
NOSCRIPT→ Ứng dụng fallback vềEVALđể nạp lại script
# Bước 1: Nạp script lên server, nhận về SHA1 hash
SCRIPT LOAD "return redis.call('SET', KEYS[1], ARGV[1])"
# Trả về: "6b1bf4865ea630eb61b4e6d61a65e8e8d9"
# Bước 2: Dùng EVALSHA thay vì EVAL (chỉ gửi hash 40 ký tự)
EVALSHA 6b1bf4865ea630eb61b4e6d61a65e8e8d9 1 mykey "myvalue"
# OK — tiết kiệm bandwidth vì không gửi lại toàn bộ script
# Nếu script chưa được cache, Redis trả lỗi:
EVALSHA "nonexistent_sha1" 0
# (error) NOSCRIPT No matching script. Please use EVAL.
# Pattern xử lý NOSCRIPT trong ứng dụng:
try:
result = r.evalsha(script_sha, 1, "mykey", "myvalue")
except redis.exceptions.NoScriptError:
# Script chưa cache, fallback EVAL
result = r.eval(script_content, 1, "mykey", "myvalue")
So sánh bandwidth: Giả sử script dài 500 bytes, gọi 10.000 lần/giây → EVAL tốn ~5MB/s bandwidth, còn EVALSHA chỉ tốn ~400KB/s (40 bytes × 10.000). Tiết kiệm hơn 12 lần!

EVALSHA — Tối ưu hiệu năng
Thay vì gửi toàn bộ script mỗi lần, bạn có thể cache script trên server và chỉ gửi SHA hash:
# Bước 1: Tính SHA1 hash của script
# (Redis tự động cache khi dùng EVAL)
# Bước 2: Dùng EVALSHA với hash
EVALSHA 6b1bf4865ea630eb61b4e6d61a65e8e8d9 1 mykey "myvalue"
# Nếu script chưa được cache, Redis trả lỗi NOSCRIPT
# Lúc đó fallback về EVAL
# SCRIPT LOAD: Nạp script trước
SCRIPT LOAD "return redis.call('SET', KEYS[1], ARGV[1])"
# "6b1bf4865ea630eb61b4e6d61a65e8e8d9"
# Sau đó dùng EVALSHA
EVALSHA 6b1bf4865ea630eb61b4e6d61a65e8e8d9 1 mykey "myvalue"
Ví dụ thực tế
1. Atomic Stock Decrease
-- Giảm stock chỉ khi còn hàng
-- KEYS[1]: product:stock
-- KEYS[2]: product:sold
-- ARGV[1]: số lượng cần giảm
local stock = tonumber(redis.call('GET', KEYS[1]) or 0)
local amount = tonumber(ARGV[1])
if stock >= amount then
redis.call('DECRBY', KEYS[1], amount)
redis.call('INCRBY', KEYS[2], amount)
return 1 -- Thành công
else
return 0 -- Không đủ hàng
end
# Sử dụng
EVAL "local stock = tonumber(redis.call('GET', KEYS[1]) or 0)
local amount = tonumber(ARGV[1])
if stock >= amount then
redis.call('DECRBY', KEYS[1], amount)
redis.call('INCRBY', KEYS[2], amount)
return 1
else
return 0
end" 2 product:555:stock product:555:sold 1
# 1 = thành công, 0 = hết hàng
2. Rate Limiter (Sliding Window)
-- Rate limiter: giới hạn N requests trong M giây
-- KEYS[1]: rate:user:1001
-- ARGV[1]: max requests (100)
-- ARGV[2]: window in seconds (60)
-- ARGV[3]: current timestamp
local key = KEYS[1]
local max_requests = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
-- Xóa requests cũ hơn window
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
-- Đếm requests hiện tại
local current = redis.call('ZCARD', key)
if current < max_requests then
-- Thêm request mới
redis.call('ZADD', key, now, now .. ':' .. math.random(1000000))
redis.call('EXPIRE', key, window)
return 1 -- Cho phép
else
return 0 -- Từ chối
end
3. Distributed Lock (Redlock đơn giản)
-- Acquire lock
-- KEYS[1]: lock key
-- ARGV[1]: unique value (process ID)
-- ARGV[2]: TTL in seconds
local lock_key = KEYS[1]
local unique_value = ARGV[1]
local ttl = tonumber(ARGV[2])
-- SET chỉ khi key chưa tồn tại (NX)
local result = redis.call('SET', lock_key, unique_value, 'NX', 'EX', ttl)
if result then
return 1 -- Lock acquired
else
return 0 -- Lock failed (đã bị chiếm)
end
-- Release lock (chỉ xóa nếu đúng owner)
-- KEYS[1]: lock key
-- ARGV[1]: unique value
local lock_key = KEYS[1]
local unique_value = ARGV[1]
if redis.call('GET', lock_key) == unique_value then
redis.call('DEL', lock_key)
return 1 -- Released
else
return 0 -- Không phải owner
end
4. Atomic Counter với Reset
-- Tăng counter, reset nếu quá giới hạn
-- KEYS[1]: counter key
-- ARGV[1]: limit
-- ARGV[2]: reset value
local current = tonumber(redis.call('INCR', KEYS[1]) or 1)
local limit = tonumber(ARGV[1])
if current > limit then
redis.call('SET', KEYS[1], ARGV[2])
return tonumber(ARGV[2])
end
return current
5. Conditional Update
-- Cập nhật chỉ khi giá trị cũ khớp
-- KEYS[1]: key
-- ARGV[1]: expected old value
-- ARGV[2]: new value
local current = redis.call('GET', KEYS[1])
if current == ARGV[1] then
redis.call('SET', KEYS[1], ARGV[2])
return 1 -- Updated
else
return 0 -- Value changed by another process
end
Sử dụng với Programming Languages
Python
import redis
r = redis.Redis()
# Script Lua
stock_script = """
local stock = tonumber(redis.call('GET', KEYS[1]) or 0)
local amount = tonumber(ARGV[1])
if stock >= amount then
redis.call('DECRBY', KEYS[1], amount)
redis.call('INCRBY', KEYS[2], amount)
return 1
else
return 0
end
"""
# Thực thi
result = r.eval(stock_script, 2, "product:555:stock", "product:555:sold", 1)
# Hoặc dùng EVALSHA (tối ưu)
script_sha = r.script_load(stock_script)
result = r.evalsha(script_sha, 2, "product:555:stock", "product:555:sold", 1)
Node.js
const redis = require('redis');
const client = redis.createClient();
// Script Lua
const rateLimitScript = `
local key = KEYS[1]
local max = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
local current = redis.call('ZCARD', key)
if current < max then
redis.call('ZADD', key, now, now .. ':' .. math.random(1000000))
redis.call('EXPIRE', key, window)
return 1
else
return 0
end
`;
// Thực thi
const allowed = await client.eval(
rateLimitScript,
{ keys: ['rate:user:1001'], arguments: ['100', '60', Date.now().toString()] }
);
Lua Scripts vs Transactions
| Tiêu chí | Transactions (MULTI/EXEC) | Lua Scripts |
|---|---|---|
| Atomicity | Partial (không isolation) | Full (true atomic) |
| Conditional logic | Cần WATCH | Tự nhiên (if/else) |
| Round-trips | Nhiều (MULTI + commands + EXEC) | 1 (EVAL) |
| Performance | Nhanh | Nhanh hơn (ít round-trip) |
| Debugging | Dễ hơn | Khó hơn |
| Use case | Batch operations đơn giản | Complex logic, conditional |
Best Practices
- Giữ script ngắn — Lua single-threaded, script dài block các client khác
- ⚠️ CẢNH BÁO: Script chạy quá lâu sẽ BLOCK toàn bộ server! — Redis chạy single-threaded, nghĩa là khi một Lua script đang thực thi, toàn bộ các client khác đều bị block, không thể xử lý bất kỳ lệnh nào. Tránh vòng lặp vô hạn, xử lý dữ liệu quá lớn, hoặc gọi các hàm tốn thời gian trong script. Nếu script chạy quá lâu, bạn có thể phải restart Redis server!
- Dùng EVALSHA — Tránh gửi lại script mỗi lần
- Không dùng biến global — Mỗi EVAL chạy trong môi trường riêng
- Kiểm tra nil — Luôn xử lý trường hợp key không tồn tại
- Script LOAD trước — Nạp script khi khởi động ứng dụng
- Test kỹ — Script lỗi có thể làm Redis crash
Bước tiếp theo
Bạn đã nắm vững Lua Scripts trong Redis! Tiếp theo, chúng ta sẽ tìm hiểu về Pipeline — cách tối ưu hiệu năng khi gửi nhiều lệnh cùng lúc.
👉 Bài tiếp theo: Pipeline trong Redis — Tối ưu hiệu năng batch operations