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

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 เก็บข้อมูลในหน่วยความจำ และคำสั่งทั่วไปจบในเวลาต่ำกว่ามิลลิวินาทีมากฝั่งเซิร์ฟเวอร์ การรันคำสั่งเป็น 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 ของแอปคุณ การรันด้านล่างคือสิ่งที่สำคัญที่สุดในโมดูลนี้

cli/patterns-index-1.redis
127.0.0.1:6379> INFO threads
# Threads
io_thread_0:clients=1,reads=375,writes=368
127.0.0.1:6379> HSET product:1 name "red running shoes" price 59
(integer) 2
127.0.0.1:6379> HSET product:2 name "blue trail shoes" price 89
(integer) 2
127.0.0.1:6379> FT.CREATE idx:products ON HASH PREFIX 1 product: SCHEMA name TEXT price NUMERIC
OK
127.0.0.1:6379> FT.SEARCH idx:products "@name:shoes @price:[50 60]" RETURN 1 name
1) (integer) 1
2) "product:1"
3) 1) "name"
2) "red running shoes"
127.0.0.1:6379> FT.DROPINDEX idx:products
OK
src/patterns-index-lost-update.ts
// @run
import { 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 -> 1
INCR, 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คำสั่งหลักกับดักที่โมดูลนี้พิสูจน์
Caching (cache-aside)GET, SET ... EX, DELSET NX ไม่หยุด stampede: miss พร้อมกัน 50 ตัว = 50 DB query
Rate limitingINCR, EXPIRE ... NX, ZSET, LuaINCR แล้ว EXPIRE เป็นสองการเรียกอาจทิ้ง counter ที่ไม่เคยรีเซ็ต
Session storehash, EXPIRE, RENAMEHSET ไม่ยืด TTL
Distributed locksSET NX PX, DELEX IFEQ / LuaDEL ธรรมดา release lock ของผู้ถือคนถัดไป

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 ที่นี่

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

Redis คือ พนักงานคนเดียวที่เคาน์เตอร์ที่มือเร็วมาก พนักงานรับทีละคำสั่ง ดังนั้นไม่มีสองคำสั่งปนกันบนเคาน์เตอร์ แต่ถ้าคุณเดินจากไปพร้อมตัวเลขในหัวแล้วกลับมาเขียน คนอื่นได้รับบริการไปแล้วในระหว่างนั้น atomicity เป็นต่อใบคำสั่ง (หนึ่งคำสั่ง หนึ่ง MULTI หนึ่งสคริปต์) ไม่ใช่ต่อลูกค้า

ตัวเลือกข้อดีข้อเสีย
ใช้ 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 ของคุณ
  • “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 (ตัวอย่างเล็กข้างบน)
ทำไม INCR เดี่ยวไม่มีทางแทรกกับคำสั่งของ client อื่น?
client 100 ตัวพร้อมกันทำ GET n, บวกหนึ่ง, SET n ผลในการรันคือ:
ฟีเจอร์ใดของ Redis ทำให้ key หายไปเองหลังหมดเวลา?
pattern ใดสร้างบน SET key token NX PX ms?
ข้อใด atomic?