Lua Scripts trong Redis: Atomic Operations nâng cao
Nhiều thao tác phức tạp như giảm stock có điều kiện hay tăng counter an toàn không thể làm bằng lệnh đơn lẻ. Lua Scripts biến cả khối logic thành một atomic operation duy nhất chạy ngay trên server.
12 phút đọc•
Mục lục bài viết19
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
Ứng dụng chạy ngon ở dev nhưng lên production, Redis vừa restart là mọi request treo 30 giây rồi trả 500. Vấn đề nằm ở cách quản lý kết nối — bài này dạy bạn cấu hình ioredis để hệ thống sống khỏe.
Ứng dụng giao đồ ăn hiện 20 quán quanh mình, sắp xếp theo khoảng cách chỉ trong mili giây — không cần database spatial hay Google Maps API, chỉ với vài lệnh Geo xây trên nền Sorted Sets.
Đếm DAU cho 10 triệu user bằng Set tốn hàng trăm MB RAM, nhưng HyperLogLog chỉ cần 12KB và Bitmap theo dõi trạng thái từng user với vỏn vẹn 1.25MB. Hai vũ khí bí mật cho analytics quy mô lớn.
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 EVALresult = 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!
Lua Scripts chạy trên Redis server — toàn bộ logic được thực thi atomic mà không bị can thiệp
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ảmlocal stock = tonumber(redis.call('GET', KEYS[1]) or0)
local amount = tonumber(ARGV[1])
if stock >= amount then
redis.call('DECRBY', KEYS[1], amount)
redis.call('INCRBY', KEYS[2], amount)
return1-- Thành côngelsereturn0-- Không đủ hàngend
# 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 timestamplocal 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ạilocal 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)
return1-- Cho phépelsereturn0-- Từ chốiend
3. Distributed Lock (Redlock đơn giản)
-- Acquire lock-- KEYS[1]: lock key-- ARGV[1]: unique value (process ID)-- ARGV[2]: TTL in secondslocal 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)localresult= redis.call('SET', lock_key, unique_value, 'NX', 'EX', ttl)
if resultthenreturn1-- Lock acquiredelsereturn0-- Lock failed (đã bị chiếm)end
-- Release lock (chỉ xóa nếu đúng owner)-- KEYS[1]: lock key-- ARGV[1]: unique valuelocal lock_key = KEYS[1]
local unique_value = ARGV[1]
if redis.call('GET', lock_key) == unique_value then
redis.call('DEL', lock_key)
return1-- Releasedelsereturn0-- Không phải ownerend
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 valuelocal current = tonumber(redis.call('INCR', KEYS[1]) or1)
local limit = tonumber(ARGV[1])
if current > limit then
redis.call('SET', KEYS[1], ARGV[2])
returntonumber(ARGV[2])
endreturn 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 valuelocalcurrent= redis.call('GET', KEYS[1])
if current== ARGV[1] then
redis.call('SET', KEYS[1], ARGV[2])
return1-- Updatedelsereturn0-- Value changed by another processend
Sử dụng với Programming Languages
Python
import redis
r = redis.Redis()
# Script Luastock_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 thiresult = 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.
0 bình luận
Đang tải bình luận...
Để lại bình luận