ข้ามไปยังเนื้อหา

Lua Scripting

Transaction จัดการการเขียนแบบกลุ่มได้ แต่ไม่สามารถแสดง conditional logic ได้ หากคุณต้องการอ่านค่า ตรวจสอบ แล้วเขียนกลับเฉพาะในเงื่อนไขบางอย่าง — ทั้งหมดนี้แบบ atomic — คุณต้องใช้ Lua script

Redis มี Lua 5.1 interpreter ในตัว script รันทั้งหมดบน server: ไม่มี network round-trip ระหว่างขั้นตอน และไม่มีคำสั่งอื่นรันได้ในขณะที่ script กำลังทำงาน

คำสั่ง EVAL รับ Lua script string จำนวน key ที่จะเข้าถึง ชื่อ key และ argument เพิ่มเติม:

EVAL script numkeys [key [key ...]] [arg [arg ...]]

ภายใน script key ถูกเข้าถึงผ่าน KEYS[1], KEYS[2], … และ argument ผ่าน ARGV[1], ARGV[2], …

127.0.0.1:6379> EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 mykey "hello"
OK
127.0.0.1:6379> GET mykey
"hello"

redis.call(command, ...) รันคำสั่ง Redis จาก Lua หากคำสั่งเกิด error redis.call จะส่ง error นั้นเป็น Lua error (ซึ่งกลายเป็น Redis error ให้ client) ใช้ redis.pcall หากต้องการ catch error ใน Lua แทน

Read-modify-write แบบ atomic ที่ง่ายที่สุด: เพิ่มค่า counter

127.0.0.1:6379> EVAL "return redis.call('INCR', KEYS[1])" 1 counter
(integer) 1
127.0.0.1:6379> EVAL "return redis.call('INCR', KEYS[1])" 1 counter
(integer) 2
127.0.0.1:6379> GET counter
"2"
EVAL "return redis.call('INCR', KEYS[1])" 1 counter
EVAL "return redis.call('INCR', KEYS[1])" 1 counter
GET counter

นี่คือ pattern ที่ set key เฉพาะเมื่อค่าปัจจุบันต่ำกว่า threshold — สิ่งที่ทำไม่ได้แบบ atomic ด้วย MULTI/EXEC เพราะต้องการ branching logic:

-- rate-limiter: เพิ่ม counter; คืน 1 ถ้าต่ำกว่า limit, 0 ถ้าเกิน
local current = redis.call('INCR', KEYS[1])
if current == 1 then
redis.call('EXPIRE', KEYS[1], ARGV[1])
end
if current <= tonumber(ARGV[2]) then
return 1
else
return 0
end
127.0.0.1:6379> EVAL "local c=redis.call('INCR',KEYS[1]) if c==1 then redis.call('EXPIRE',KEYS[1],ARGV[1]) end if c<=tonumber(ARGV[2]) then return 1 else return 0 end" 1 ratelimit:user1 60 5
(integer) 1

การส่ง script text เต็มทุก request เปลือง bandwidth SCRIPT LOAD อัปโหลด script ครั้งเดียวและคืน SHA1 hash แล้วคุณเรียกใช้ด้วย EVALSHA:

127.0.0.1:6379> SCRIPT LOAD "return redis.call('INCR', KEYS[1])"
"2068d9c36e28e0f50d70bedb00173c36e5f4f39d"
127.0.0.1:6379> EVALSHA 2068d9c36e28e0f50d70bedb00173c36e5f4f39d 1 counter
(integer) 3

script ถูก cache ใน Redis memory ตลอด lifetime ของ server (หรือจนกว่าจะ SCRIPT FLUSH) หาก script ไม่ถูกพบ — เช่น หลัง server restart — EVALSHA คืน error NOSCRIPT และ client ควร fallback ไปใช้ EVAL เพื่ออัปโหลดใหม่

SCRIPT LOAD "return redis.call('INCR', KEYS[1])"
EVALSHA 2068d9c36e28e0f50d70bedb00173c36e5f4f39d 1 counter
GET counter
ตัวเลือกBenefitCost
Lua script (EVAL) แทน MULTI/EXECทำ conditional/branching logic แบบ atomic ได้ — สิ่งที่ transaction ทำไม่ได้เขียนและ debug ยากกว่าคำสั่งเดี่ยว ต้องแยก Lua error จาก Redis error เอง
Cache script ด้วย EVALSHAไม่ต้องส่ง script text เต็มซ้ำทุก request ประหยัด bandwidthต้อง handle NOSCRIPT fallback เอง โดยเฉพาะหลัง server restart หรือ SCRIPT FLUSH
Script รัน atomic บน serverไม่มีคำสั่งอื่นแทรกระหว่าง script ทำงาน รับประกัน consistencyScript ที่ยาวหรือมี loop หนักจะบล็อก event loop ทำให้ client อื่นทั้งหมดรอ
  • เขียน Lua script ที่ทำงานหนักหรือวนลูปจำนวนมาก — Redis เป็น single-threaded ดังนั้น script ที่ช้าจะบล็อกทุกคำสั่งอื่นบน server ทั้งหมด ควรจำกัด script ให้สั้นและ predictable
  • ไม่ handle error NOSCRIPT จาก EVALSHA — หลัง server restart หรือ SCRIPT FLUSH hash ที่ cache ไว้จะหายไป client ต้องตรวจ error นี้แล้ว fallback ไปใช้ EVAL เพื่ออัปโหลด script ใหม่
  • ใช้ redis.call ทั้งที่ต้องการ handle error เองredis.call จะโยน Lua error ทันทีเมื่อคำสั่งล้มเหลว ทำให้ script ทั้งหมดหยุด ถ้าต้องการตรวจสอบและจัดการ error ภายใน script ให้ใช้ redis.pcall แทน

💡 ตัวอย่างจากของจริง

Stripe — ใช้ Lua script บน Redis เพื่อทำ atomic idempotency check ก่อนประมวลผล payment request ซ้ำ ป้องกัน race condition ระหว่างการอ่านและเขียนสถานะ

Rate limiter ทั่วไปในหลายบริษัท — ใช้ pattern แบบเดียวกับในบทนี้ (INCR + EXPIRE + threshold check) ห่อด้วย Lua เพื่อให้การตรวจสอบและอัปเดต counter เป็น atomic operation เดียว

ภายใน Lua script คุณเข้าถึง key แรกที่ส่งให้ EVAL อย่างไร?
SCRIPT LOAD คืนค่าอะไร?
EVALSHA คืน error อะไรเมื่อ script ไม่ได้ถูก cache ไว้?
Lua script ที่รันด้วย EVAL เป็น atomic เพราะ: