Patterns Overview
ทดสอบบน Redis 8.10.2 (
redis:8.10.2-alpine), ioredis 6.0.0, Node 24 บน Mac (Apple silicon) transcript แบบ# cli/...คือ session เดียวเรียงตามเอกสารที่ harness ของคอร์สรันซ้ำ
Redis ไม่ใช่แค่ cache: มันคือ data-structure server ที่แก้ปัญหากลุ่มหนึ่งที่ relational database ทำได้ไม่ดี ส่วนใหญ่เพราะคำตอบต้อง เร็ว ใช้ร่วมกัน ระหว่างเซิร์ฟเวอร์ และ อยู่สั้น โมดูลนี้พาผ่านสี่ pattern และพิสูจน์แต่ละอันกับเซิร์ฟเวอร์จริง
ทำไม Redis เร็ว และ “atomic” ครอบคลุมแค่ไหนจริงๆ
หัวข้อที่มีชื่อว่า “ทำไม Redis เร็ว และ “atomic” ครอบคลุมแค่ไหนจริงๆ”Redis เก็บข้อมูลในหน่วยความจำ และคำสั่งทั่วไปจบในเวลาต่ำกว่ามิลลิวินาทีมากฝั่งเซิร์ฟเวอร์ การรันคำสั่งเป็น single-thread; ตัว process ไม่ใช่ thread เดียวรันคำสั่งของคุณทีละคำสั่ง ซึ่งเป็นเหตุผลที่คำสั่งเดี่ยวอย่าง INCR ไม่มีทางแทรกกับของ client อื่น Redis 8 ยังรัน I/O thread และ background thread สำหรับงานอย่างการปิดไฟล์ AOF fsync และ lazy free (INFO threads ด้านล่างแสดง I/O thread; ถ้อยคำเดิม “Redis เป็น single-thread” จริงเฉพาะการรันคำสั่ง)
ข้อควรระวังคือ atomicity เป็นของ หนึ่งคำสั่ง (หรือหนึ่ง MULTI/EXEC หรือหนึ่งสคริปต์) ไม่ใช่ลำดับ read-then-write ของแอปคุณ การรันด้านล่างคือสิ่งที่สำคัญที่สุดในโมดูลนี้
127.0.0.1:6379> INFO threads# Threadsio_thread_0:clients=1,reads=375,writes=368127.0.0.1:6379> HSET product:1 name "red running shoes" price 59(integer) 2127.0.0.1:6379> HSET product:2 name "blue trail shoes" price 89(integer) 2127.0.0.1:6379> FT.CREATE idx:products ON HASH PREFIX 1 product: SCHEMA name TEXT price NUMERICOK127.0.0.1:6379> FT.SEARCH idx:products "@name:shoes @price:[50 60]" RETURN 1 name1) (integer) 12) "product:1"3) 1) "name" 2) "red running shoes"127.0.0.1:6379> FT.DROPINDEX idx:productsOK// @runimport { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);const N = 100;
// Every Redis command is atomic, but GET-then-SET is TWO commands: other clients run in between.async function badIncrement(key: string) { const current = Number((await redis.get(key)) ?? 0); await redis.set(key, String(current + 1));}
await redis.set('b3:lost', '0');await Promise.all(Array.from({ length: N }, () => badIncrement('b3:lost')));console.log(`GET + SET, ${N} concurrent increments -> ${await redis.get('b3:lost')}`);
await redis.set('b3:kept', '0');await Promise.all(Array.from({ length: N }, () => redis.incr('b3:kept')));console.log(`INCR, ${N} concurrent increments -> ${await redis.get('b3:kept')}`);
await redis.del('b3:lost', 'b3:kept');await redis.quit();ผลลัพธ์จริง (Mac เครื่องนี้):
GET + SET, 100 concurrent increments -> 1INCR, 100 concurrent increments -> 100วงจร “อ่าน บวกหนึ่ง เขียนกลับ” พร้อมกัน 100 ครั้งให้ผล 1: บน connection เดียว GET ทั้ง 100 ถูกส่งก่อนที่ SET ใดๆ จะถูกส่ง ทุกวงจรจึงอ่านได้ 0 ทุกคำสั่งเป็น atomic แต่ผลยังผิด เพราะ ลำดับ ไม่ atomic INCR ทำ read-modify-write ในคำสั่งเดียวและได้ 100 ส่วนที่เหลือของโมดูลคือการปิดช่องว่างนี้: SET ... NX สำหรับ lock, INCR บวก EXPIRE NX ใน MULTI สำหรับ counter และ Lua เมื่อการตัดสินใจมีหลายขั้น (Transactions & Scripting)
Redis ยังรองรับ TTL: ทุก key ตั้งเวลาหมดอายุได้ key ที่หมดอายุถูกลบแบบ lazy เมื่อถูกแตะ บวกการกวาดใน background cache และ session จึงทำความสะอาดตัวเอง เวลาที่แน่นอนอยู่ใน Expiration & TTL
Pattern ในโมดูลนี้
หัวข้อที่มีชื่อว่า “Pattern ในโมดูลนี้”| Pattern | คำสั่งหลัก | กับดักที่โมดูลนี้พิสูจน์ |
|---|---|---|
| Caching (cache-aside) | GET, SET ... EX, DEL | SET NX ไม่หยุด stampede: miss พร้อมกัน 50 ตัว = 50 DB query |
| Rate limiting | INCR, EXPIRE ... NX, ZSET, Lua | INCR แล้ว EXPIRE เป็นสองการเรียกอาจทิ้ง counter ที่ไม่เคยรีเซ็ต |
| Session store | hash, EXPIRE, RENAME | HSET ไม่ยืด TTL |
| Distributed locks | SET NX PX, DELEX IFEQ / Lua | DEL ธรรมดา release lock ของผู้ถือคนถัดไป |
ความสามารถเพิ่มอีกสองอย่างใน Redis 8
หัวข้อที่มีชื่อว่า “ความสามารถเพิ่มอีกสองอย่างใน Redis 8”Redis 8 รวม module เสริมเดิมเข้ามาในเซิร์ฟเวอร์ (What’s new in Redis 8.0) การรันข้างบนจึงสร้าง search index บน hash ที่มีอยู่ด้วย FT.CREATE และ query ด้วย FT.SEARCH (คำข้อความบวกช่วงตัวเลข) โดยไม่ต้องติดตั้งอะไร ชุดความสามารถเดียวกันมี vector field สำหรับ similarity search และ Redis 8 เพิ่ม vector set ซึ่งเป็น data type เฉพาะที่ release note ระบุว่า beta ใช้สำหรับการทดลอง ไม่ใช่โค้ดที่ย้ายไม่ได้ ทั้งสองอย่างไม่ได้แทนที่สี่ pattern ที่นี่
Reading AI-generated code
หัวข้อที่มีชื่อว่า “Reading AI-generated code”AI เขียนตัวนับ page view และให้เหตุผลว่า “Redis เป็น single-thread ดังนั้นปลอดภัย”:
🐛 หา bug ในโค้ดนี้
async function recordView(pageId: string) {const views = Number((await redis.get(`views:${pageId}`)) ?? 0);await redis.set(`views:${pageId}`, String(views + 1)); // safe: Redis is single-threaded}เฉลย
แต่ละคำสั่ง atomic แต่สองคำสั่งไม่ใช่ ผู้เรียกพร้อมกันอ่านค่าเก่าค่าเดียวกันและเขียนทับการอัปเดตของกันและกัน: ในการรันข้างบน การเพิ่มพร้อมกัน 100 ครั้งจบที่ 1 ใช้คำสั่งที่ทำ read-modify-write ฝั่งเซิร์ฟเวอร์:
await redis.incr(`views:${pageId}`); // 100 concurrent calls -> 100สำหรับอะไรที่ซับซ้อนกว่า counter (ตรวจค่าแล้วตัดสินใจ) ใช้ WATCH สคริปต์ Lua หรือคำสั่งเดี่ยวแบบ SET ... NX
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”Redis คือ พนักงานคนเดียวที่เคาน์เตอร์ที่มือเร็วมาก พนักงานรับทีละคำสั่ง ดังนั้นไม่มีสองคำสั่งปนกันบนเคาน์เตอร์ แต่ถ้าคุณเดินจากไปพร้อมตัวเลขในหัวแล้วกลับมาเขียน คนอื่นได้รับบริการไปแล้วในระหว่างนั้น atomicity เป็นต่อใบคำสั่ง (หนึ่งคำสั่ง หนึ่ง MULTI หนึ่งสคริปต์) ไม่ใช่ต่อลูกค้า
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| ตัวเลือก | ข้อดี | ข้อเสีย |
|---|---|---|
| ใช้ Redis สำหรับ pattern เหล่านี้ | ต่ำกว่ามิลลิวินาที ใช้ร่วมกันข้ามเซิร์ฟเวอร์ มี TTL ในตัว | ข้อมูลอยู่ในหน่วยความจำ ต้องออกแบบให้รับมือการสูญหายและ eviction |
| atomicity ของคำสั่ง/สคริปต์เดี่ยว | ไม่มี lock ไม่มี race ภายในหน่วย | อะไรที่คร่อมหลายคำสั่งต้องใช้ MULTI, WATCH หรือ Lua |
| instance เดียวสำหรับ cache, session, limit และ lock | ต้องรันน้อยลง | tenant ที่ก่อกวน (key ใหญ่, stampede) ทำร้ายทุก pattern; แยก instance หรือตั้ง eviction ถ้าสำคัญ |
| ฟีเจอร์ search และ vector ในตัว | ไม่ต้องติดตั้งบริการเพิ่ม | ใช้หน่วยความจำและ CPU ร่วมกับ pattern ของคุณ |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “Redis เป็น single-thread โค้ดฉันจึงไม่มี race” การรันคำสั่งเป็น single-thread; read-then-write ข้ามคำสั่งของคุณไม่ atomic (1 จาก 100 ข้างบน)
- “Redis ไม่มี parallelism เลย” มี I/O thread และ background thread; มีแค่การรันคำสั่งที่ถูก serialize
- “หมดอายุแปลว่าหายทันที” key ที่หมดอายุถูกลบแบบ lazy และโดยการกวาด
- “ต้องมี search engine แยกสำหรับการค้นหาที่เกินกว่า key”
FT.CREATEทำ index ให้ hash และ JSON ใน Redis 8 (ตัวอย่างเล็กข้างบน)