Transactions, Lua & Tooling: Overview
ทดสอบบน Redis 8.10.2 (
redis:8.10.2-alpine), ioredis 6.0.0, Node 24 บน Mac (Apple silicon) ผลลัพธ์ของสคริปต์มาจากการรันของผม
โมดูลนี้ครอบคลุมอะไร
หัวข้อที่มีชื่อว่า “โมดูลนี้ครอบคลุมอะไร”Redis ไปไกลกว่าการทำงานแบบ key-value ธรรมดา เมื่อแอปโตขึ้น คุณต้องรันหลายคำสั่งโดยไม่มีการรบกวน ย้าย logic ไปไว้ใกล้ข้อมูล และวินิจฉัยเซิร์ฟเวอร์เมื่อช้าลง
| บทเรียน | หัวข้อ |
|---|---|
| Transactions | MULTI/EXEC/DISCARD, EXECABORT, ไม่มี rollback, optimistic locking ด้วย WATCH |
| Pipelines | รวม round trip (SET 5000 ครั้งใช้ 17 ms เทียบ 1688 ms), redis-cli --pipe, result tuple |
| Lua Scripting | EVAL, redis.call/pcall, script cache ฝั่งเซิร์ฟเวอร์, SCRIPT KILL, Functions |
| Tooling & Monitoring | INFO, SLOWLOG, --bigkeys/--keystats, MEMORY, LATENCY, ต้นทุนของ MONITOR |
ทำไมการจัดกลุ่มคำสั่งจึงสำคัญ
หัวข้อที่มีชื่อว่า “ทำไมการจัดกลุ่มคำสั่งจึงสำคัญ”การรันคำสั่งเป็น single-thread; ตัว process ไม่ใช่ Redis รันทีละคำสั่ง คำสั่งเดี่ยวจึงไม่มีทางแทรกกับอีกคำสั่ง แต่แอปของคุณมักต้องใช้หลายคำสั่ง และระหว่างสองคำสั่ง client อื่นรันแทรกได้ มีสามเครื่องมือ และแต่ละอันแก้คนละปัญหา:
- Transactions (
MULTI/EXEC) เข้าคิวแล้วรันเป็นบล็อกเดียวที่ไม่ถูกขัด แตกกิ่งตามผลระหว่างทางไม่ได้และไม่มี rollback - Pipelines ส่งหลายคำสั่งใน network round trip เดียว ได้ความเร็ว ไม่ใช่ atomicity และ error ต่อคำสั่งกลับมาในอาร์เรย์คำตอบ
- สคริปต์ Lua รันทั้งหมดบนเซิร์ฟเวอร์แบบ atomic แตกกิ่งได้และใช้คำสั่งได้ครบ มันบล็อกเซิร์ฟเวอร์ขณะรัน
ทำไมถึงสำคัญ: ผู้ซื้อ 20 คน ของ 5 ชิ้น
หัวข้อที่มีชื่อว่า “ทำไมถึงสำคัญ: ผู้ซื้อ 20 คน ของ 5 ชิ้น”สคริปต์ด้านล่างขายของ 5 ชิ้นให้ผู้ซื้อ 20 คนพร้อมกันสองครั้ง เวอร์ชันแรกตรวจ stock แล้วลดใน pipeline (ความเชื่อที่พบบ่อยคือ pipeline “ทำให้ atomic”) เวอร์ชันที่สองตรวจและลดภายในสคริปต์ Lua เดียว
// @runimport { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);const BUYERS = 20;const STOCK = 5;
// "check, then decrement" as two steps. Wrapping the second step in a pipeline does not help:// the check already happened, and other buyers ran their checks before any decrement arrived.async function buyPipelined() { const stock = Number(await redis.get('b3:tsl:stock')); if (stock > 0) await redis.pipeline().decr('b3:tsl:stock').exec();}
// the same decision inside one Lua script: read, compare and write cannot be interleavedredis.defineCommand('buyLua', { numberOfKeys: 1, lua: `local s = tonumber(redis.call('GET', KEYS[1])) if s and s > 0 then redis.call('DECR', KEYS[1]) return 1 end return 0`,});declare module 'ioredis' { interface RedisCommander<Context> { buyLua(key: string): Promise<number>; }}
await redis.set('b3:tsl:stock', String(STOCK));await Promise.all(Array.from({ length: BUYERS }, buyPipelined));console.log(`GET then pipelined DECR : ${BUYERS} buyers, stock ${STOCK} -> stock left ${await redis.get('b3:tsl:stock')}`);
await redis.set('b3:tsl:stock', String(STOCK));const sold = (await Promise.all(Array.from({ length: BUYERS }, () => redis.buyLua('b3:tsl:stock')))).reduce((a, b) => a + b, 0);console.log(`Lua check-and-decrement : ${BUYERS} buyers, stock ${STOCK} -> sold ${sold}, stock left ${await redis.get('b3:tsl:stock')}`);
await redis.del('b3:tsl:stock');await redis.quit();ผลลัพธ์จริง (Mac เครื่องนี้):
GET then pipelined DECR : 20 buyers, stock 5 -> stock left -15Lua check-and-decrement : 20 buyers, stock 5 -> sold 5, stock left 0เวอร์ชัน pipeline ขายของ 20 ชิ้นที่ไม่มี (stock เหลือ -15): ผู้ซื้อทั้ง 20 ผ่านการตรวจ stock > 0 ก่อนที่การลดใดๆ จะมาถึง สคริปต์ Lua ขายพอดี 5 ชิ้น MULTI/WATCH ก็ให้ 5 พอดีเช่นกัน โดยแลกกับการลองซ้ำ (Transactions)
ทำไมเครื่องมือจึงสำคัญ
หัวข้อที่มีชื่อว่า “ทำไมเครื่องมือจึงสำคัญ”แอปที่ถูกต้องก็ยังเจอ query ช้า key ใหญ่เกินไป หรือ connection รั่วได้ Redis มาพร้อมเครื่องมือวินิจฉัยที่เปิดทิ้งได้อย่างปลอดภัย (INFO, SLOWLOG, --keystats) และหัววัดที่ไม่ปลอดภัย (KEYS, MONITOR) บทเครื่องมือแสดง output จริงและวัดต้นทุนของตัวที่เสี่ยง
PING
INFO serverReading AI-generated code
หัวข้อที่มีชื่อว่า “Reading AI-generated code”AI ถูกขอให้ทำ “ตรวจ stock แล้วลด” ให้ปลอดภัยและหยิบ pipeline มาใช้:
🐛 หา bug ในโค้ดนี้
async function buy() {const stock = Number(await redis.get('stock'));if (stock > 0) { await redis.pipeline().decr('stock').exec(); // "the pipeline makes the decrement atomic"}}เฉลย
pipeline รวมเฉพาะ I/O เครือข่ายของคำสั่ง ที่อยู่ข้างใน GET และการตรวจเกิดก่อนหน้านั้น ผู้ซื้อ 20 คนพร้อมกันจึงเห็น stock = 5 ทั้งหมด ผ่าน stock > 0 ทั้งหมด และลดทั้งหมด: stock จบที่ -15 ใส่การอ่าน การเปรียบเทียบ และการเขียนในหน่วย atomic เดียว ที่นี่คือสคริปต์ Lua (หรือ WATCH + MULTI พร้อม retry loop หรือ DECR ธรรมดาตามด้วยการตรวจแล้วยกเลิก):
redis.defineCommand('buy', { numberOfKeys: 1, lua: `local s = tonumber(redis.call('GET', KEYS[1])) if s and s > 0 then redis.call('DECR', KEYS[1]) return 1 end return 0`,});Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”ลองคิดเป็นสามวิธีส่งงานให้พนักงานคนเดียวที่เร็วมาก pipeline คือสลิปเป็นปึกที่ยื่นในเที่ยวเดียว: เร็ว แต่พนักงานอาจให้บริการคนอื่นคั่นระหว่างสลิปของคุณ transaction คือซองปิดผนึก: สลิปข้างในถูกประมวลผลด้วยกัน แต่พนักงานอ่านคำตอบของสลิปหนึ่งเพื่อตัดสินใจสลิปถัดไปไม่ได้ สคริปต์ คือขั้นตอนที่เขียนไว้ซึ่งพนักงานทำตามขณะปิดเคาน์เตอร์: แตกกิ่งได้และ atomic แต่ทุกคนรอขณะพนักงานทำตาม
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| ตัวเลือก | ข้อดี | ข้อเสีย |
|---|---|---|
| Pipeline | round trip น้อยที่สุด throughput สูงสุด | ไม่ atomic; error ซ่อนในอาร์เรย์คำตอบ |
MULTI/EXEC | รันต่อเนื่องกัน | แตกกิ่งไม่ได้ ไม่มี rollback |
WATCH + MULTI | check-and-set โดยไม่ต้อง lock | ต้องมี retry loop และหนึ่ง connection ต่อหนึ่งครั้งที่ลอง |
| สคริปต์ Lua / Function | logic แตกกิ่งแบบ atomic | บล็อกเซิร์ฟเวอร์ขณะรัน; ต้องสั้น |
| เครื่องมือวินิจฉัยในตัว | ไม่ต้องใช้ซอฟต์แวร์เพิ่ม | หัววัดที่เสี่ยง (KEYS, MONITOR) มีต้นทุนที่วัดได้ |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “pipeline คือ transaction” มันแค่รวม round trip; การรัน oversell ข้างบนจบที่ -15
- “Redis เป็น single-thread ดังนั้น logic หลายขั้นของฉัน atomic” มีแค่แต่ละคำสั่ง (หรือ transaction หรือสคริปต์) ที่ atomic
- “สคริปต์ Lua ไม่มีต้นทุน” มันบล็อก client อื่นทุกตัวขณะรัน
- “อะไรก็ตามใน
redis-cliปลอดภัยบน production”KEYSและMONITORไม่ปลอดภัย (วัดในบทเครื่องมือ)