Lua Scripting
ทำไมต้องใช้ Lua scripting?
หัวข้อที่มีชื่อว่า “ทำไมต้องใช้ Lua scripting?”Transaction จัดการการเขียนแบบกลุ่มได้ แต่ไม่สามารถแสดง conditional logic ได้ หากคุณต้องการอ่านค่า ตรวจสอบ แล้วเขียนกลับเฉพาะในเงื่อนไขบางอย่าง — ทั้งหมดนี้แบบ atomic — คุณต้องใช้ Lua script
Redis มี Lua 5.1 interpreter ในตัว script รันทั้งหมดบน server: ไม่มี network round-trip ระหว่างขั้นตอน และไม่มีคำสั่งอื่นรันได้ในขณะที่ script กำลังทำงาน
EVAL — รัน Lua script
หัวข้อที่มีชื่อว่า “EVAL — รัน Lua 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"OK127.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 แทน
ตัวอย่าง INCR แบบปฏิบัติ
หัวข้อที่มีชื่อว่า “ตัวอย่าง INCR แบบปฏิบัติ”Read-modify-write แบบ atomic ที่ง่ายที่สุด: เพิ่มค่า counter
127.0.0.1:6379> EVAL "return redis.call('INCR', KEYS[1])" 1 counter(integer) 1127.0.0.1:6379> EVAL "return redis.call('INCR', KEYS[1])" 1 counter(integer) 2127.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 counterConditional logic ใน Lua
หัวข้อที่มีชื่อว่า “Conditional logic ใน Lua”นี่คือ 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])endif current <= tonumber(ARGV[2]) then return 1else return 0end127.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) 1SCRIPT LOAD และ EVALSHA
หัวข้อที่มีชื่อว่า “SCRIPT LOAD และ EVALSHA”การส่ง 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) 3script ถูก 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ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
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 ทำงาน รับประกัน consistency | Script ที่ยาวหรือมี loop หนักจะบล็อก event loop ทำให้ client อื่นทั้งหมดรอ |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- เขียน Lua script ที่ทำงานหนักหรือวนลูปจำนวนมาก — Redis เป็น single-threaded ดังนั้น script ที่ช้าจะบล็อกทุกคำสั่งอื่นบน server ทั้งหมด ควรจำกัด script ให้สั้นและ predictable
- ไม่ handle error
NOSCRIPTจากEVALSHA— หลัง server restart หรือSCRIPT FLUSHhash ที่ 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 เดียว