คำสั่งแรกของคุณ
ทดสอบบน Redis 8.10.2 (
redis:8.10.2-alpine), ioredis 6.0.0, Node 24 บน Mac (Apple silicon) transcript ด้านล่างเป็น session เดียวเรียงตามลำดับเอกสาร (ทุก fence ที่ขึ้นต้น# cli/...ถูก harness ของคอร์สรันซ้ำ) และตัวเลขเวลาวัดบนเครื่องนี้
Key-value mental model
หัวข้อที่มีชื่อว่า “Key-value mental model”โดยแก่นแล้ว Redis คือ key-value store ข้อมูลทุกชิ้นอยู่ภายใต้ key key เป็น string เสมอ (binary-safe ยาวได้ถึง 512 MB แต่ควรตั้งให้สั้น) ค่ามี type — string, list, hash, set, sorted set หรือ stream — และ type เป็นตัวกำหนดว่ารันคำสั่งไหนกับค่านั้นได้
ลองนึกภาพเป็นพจนานุกรมขนาดใหญ่: ค้นด้วย key ได้ value กลับมา และยังถามได้ว่า key มีอยู่ไหม value เป็น type อะไร หรือลบมันทิ้งได้
SET และ GET
หัวข้อที่มีชื่อว่า “SET และ GET”SET key value เก็บค่า GET key ดึงค่ากลับมา ถ้า key มีอยู่แล้ว SET จะเขียนทับ
127.0.0.1:6379> SET name "Redis"OK127.0.0.1:6379> GET name"Redis"127.0.0.1:6379> SET score 100OKEXISTS — ตรวจว่า key มีอยู่ไหม
หัวข้อที่มีชื่อว่า “EXISTS — ตรวจว่า key มีอยู่ไหม”EXISTS key คืน 1 ถ้ามี 0 ถ้าไม่มี ส่งหลาย key ได้ — ค่าที่คืนคือจำนวน key ที่มีอยู่ (key ที่ระบุซ้ำสองครั้งนับสองครั้ง)
127.0.0.1:6379> EXISTS name(integer) 1127.0.0.1:6379> EXISTS missing(integer) 0127.0.0.1:6379> EXISTS name score missing(integer) 2TYPE — ตรวจ type ของ value
หัวข้อที่มีชื่อว่า “TYPE — ตรวจ type ของ value”TYPE key คืน type ของ value ที่เก็บใน key: string, list, hash, set, zset หรือ stream และคืน none ถ้า key ไม่มีอยู่
127.0.0.1:6379> TYPE namestring127.0.0.1:6379> TYPE missingnoneKEYS — แสดง key ตาม pattern
หัวข้อที่มีชื่อว่า “KEYS — แสดง key ตาม pattern”KEYS pattern คืนทุก key ที่ตรงกับ glob pattern KEYS * ตรงกับทุกอย่าง ลำดับเป็นอะไรก็ได้ที่ hash table ภายในให้มา — อย่าพึ่งพาลำดับนี้
127.0.0.1:6379> KEYS *1) "score"2) "name"คำเตือน: ห้ามใช้
KEYS *ใน production การรันคำสั่งเป็น single-threaded —KEYSบล็อกเซิร์ฟเวอร์ทั้งตัวจนกว่าจะสแกนทุก key เสร็จ ใช้SCANแทน ซึ่งวนทีละ batch เล็ก ๆ หัวข้อถัดไปวัดว่ามันแพงเท่าไร
DEL — ลบ key หนึ่งตัวหรือมากกว่า
หัวข้อที่มีชื่อว่า “DEL — ลบ key หนึ่งตัวหรือมากกว่า”DEL key [key ...] ลบ key หนึ่งตัวหรือหลายตัว และคืน จำนวน key ที่ถูกลบจริง ถ้า key ไม่มีอยู่ก็แค่ข้าม และไม่นับรวมในค่าที่คืน
127.0.0.1:6379> DEL name(integer) 1127.0.0.1:6379> GET name(nil)127.0.0.1:6379> DEL name score(integer) 1DEL ตัวที่สองระบุ key ที่หายไปแล้วหนึ่งตัวกับที่ยังอยู่หนึ่งตัว จึงคืน 1 ไม่ใช่ 2
SET name "Redis"
GET name
SET score 100
EXISTS name
EXISTS missing
TYPE name
KEYS *
DEL name
GET nameทั้งหกชนิด และ Redis เก็บมันอย่างไร
หัวข้อที่มีชื่อว่า “ทั้งหกชนิด และ Redis เก็บมันอย่างไร”หนึ่ง key ต่อหนึ่ง type แล้วใช้ TYPE และ OBJECT ENCODING กับแต่ละตัว (encoding คือวิธีที่ Redis จัดวางค่าในหน่วยความจำ — ค่าเล็กใช้ listpack ซึ่งเป็น array แบบกะทัดรัด และจะเห็นการสลับไปโครงสร้างใหญ่ขึ้นในบทถัด ๆ ไป):
127.0.0.1:6379> SET user:42:name "Ada"OK127.0.0.1:6379> RPUSH user:42:events login view(integer) 2127.0.0.1:6379> HSET user:42 name Ada plan pro(integer) 2127.0.0.1:6379> SADD user:42:tags admin beta(integer) 2127.0.0.1:6379> ZADD user:42:score 100 Ada(integer) 1127.0.0.1:6379> XADD user:42:log * action login"1790747304943-0"127.0.0.1:6379> TYPE user:42:eventslist127.0.0.1:6379> TYPE user:42hash127.0.0.1:6379> TYPE user:42:tagsset127.0.0.1:6379> TYPE user:42:scorezset127.0.0.1:6379> TYPE user:42:logstream127.0.0.1:6379> OBJECT ENCODING user:42:name"embstr"127.0.0.1:6379> OBJECT ENCODING user:42:events"listpack"127.0.0.1:6379> GET user:42:events(error) WRONGTYPE Operation against a key holding the wrong kind of valueมีสองอย่างให้สังเกต ID ของ stream ขึ้นต้นด้วยนาฬิกาเป็นมิลลิวินาทีของเซิร์ฟเวอร์ (1790747304943 ในที่นี้; ของคุณจะต่างออกไป) และบรรทัดสุดท้ายคือระบบ type กัดกลับ: GET บน list ถูกปฏิเสธด้วย WRONGTYPE ดังนั้น type ของ key คือสัญญาที่โค้ดของคุณต้องรักษา
ต้นทุนของ KEYS ที่วัดจริง
หัวข้อที่มีชื่อว่า “ต้นทุนของ KEYS ที่วัดจริง”“KEYS บล็อกเซิร์ฟเวอร์” เป็นข้อกล่าวอ้างที่วัดได้ ผมโหลด key 200,000 แล้ว 1,000,000 ตัว (b1:key:<n>) ลงใน container 8.10.2 ที่สะอาด แล้วรัน KEYS 'b1:key:*' โดยตั้ง slowlog-log-slower-than 0 เพื่อให้เซิร์ฟเวอร์รายงานเองว่าคำสั่งถือ command thread นานเท่าไร:
| จำนวน key ใน database | เวลาฝั่งเซิร์ฟเวอร์ของ KEYS (SLOWLOG) | การเรียก SCAN ... COUNT 1000 เดี่ยวที่นานที่สุด |
|---|---|---|
| 200,000 | 15.8 ms | — |
| 1,000,000 | 155.9 ms | 1.8 ms |
ตลอด 156 ms นั้น ไม่มีอะไรอื่นรันเลย: connection ที่สองที่ PING ทุกมิลลิวินาทีเห็น round trip ที่แย่ที่สุดเพิ่มจาก 2.4 ms (ตอนว่าง) เป็น 17.6 ms ในกรณี 200k; เวลาจริงฝั่ง client ของการเรียก KEYS ทั้งหมด (รวมการส่งและ parse string 200k ตัว) คือ 121 ms SCAN เดิน 1M key ครบใน 1,000 การเรียก แต่ละครั้งไม่เกิน 1.8 ms; เวลารวมนานกว่า (1.3 วินาที เทียบกับ 0.6 วินาทีของ KEYS) — คุณแลกเวลารวมกับการไม่ทำให้ใครค้าง มันเพิ่มเป็นเส้นตรง สิบล้าน key จึงหมายถึง Redis ค้างเกินหนึ่งวินาที บท Scan Keys ลงลึกเรื่อง SCAN เอง
script client แรกของคุณ
หัวข้อที่มีชื่อว่า “script client แรกของคุณ”CLI ไว้สำรวจ; แอปใช้ client library คำสั่งเดียวกันผ่าน ioredis 6.0.0 — สังเกตว่าค่าที่คืนหน้าตาเป็นอย่างไรใน JavaScript:
// @runimport { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);redis.on('error', (err) => console.error('redis error:', err.message));
await redis.set('b1:name', 'Redis');console.log(await redis.get('b1:name')); // "Redis"console.log(await redis.get('b1:missing')); // null (the CLI's (nil))console.log(await redis.exists('b1:name', 'b1:missing')); // 1 (a number, not a string)console.log(await redis.type('b1:name')); // "string"console.log(await redis.del('b1:name')); // 1
await redis.quit(); // waits for pending replies, then closesoutput เมื่อรัน: Redis, null, 1, string, 1 key ที่ไม่มีคือ null, integer เป็น number และ quit() (ไม่ใช่ disconnect()) ปล่อยให้คำสั่งที่ค้างอยู่จบก่อน listener error สำคัญ: ถ้าไม่มี เซิร์ฟเวอร์ที่เข้าถึงไม่ได้จะทำให้เกิด event error ที่ไม่มีใครดักจับ
แนวปฏิบัติที่ดีในการตั้งชื่อ key
หัวข้อที่มีชื่อว่า “แนวปฏิบัติที่ดีในการตั้งชื่อ key”key เป็นแค่ string จึงตั้งชื่ออะไรก็ได้ — แต่ชื่อที่มีความหมายทำให้ debug และ monitor ง่ายขึ้นมาก convention มาตรฐานคือ namespace คั่นด้วย colon:
| รูปแบบ | ตัวอย่าง |
|---|---|
object:id | user:42 |
object:id:field | user:42:email |
type:identifier | session:abc123 |
วิธีนี้ทำให้จัดกลุ่ม key ที่เกี่ยวข้อง ใช้ glob pattern อย่างปลอดภัย และเห็นได้ทันทีว่า key เก็บอะไรในเครื่องมืออย่าง RedisInsight
Reading AI-generated code
หัวข้อที่มีชื่อว่า “Reading AI-generated code”AI ถูกขอให้ทำ endpoint “ผู้ใช้ออนไลน์” แล้วเขียนว่า:
🐛 หา bug ในโค้ดนี้
app.get('/online', async (_req, res) => {// count active sessionsconst keys = await redis.keys('session:*');res.json({ online: keys.length });});เฉลย
KEYS ใน request handler คือ bug ทุกครั้งที่เรียกจะทำให้ Redis ค้างตลอดการสแกน: เราวัดได้ 16 ms ที่ 200k key และ 156 ms ที่ 1M — และทุก request ที่มาพร้อมกันเพิ่มการสแกนเต็มอีกรอบ endpoint ที่คนใช้เยอะจึงทำให้คำสั่งของคนอื่นต่อคิวรอ มันยังส่งชื่อ key ทุกตัวผ่านเครือข่ายทั้งที่ต้องการแค่นับ วิธีแก้คือเดิน keyspace ด้วย SCAN ทีละ batch เล็ก ๆ — หรือถ้าเป็นตัวนับที่อ่านบ่อย ให้เก็บจำนวนเป็นตัวเลขของมันเอง (INCR/DECR ในบท Counters) หรือ Set ของ session id:
// @runimport { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);
const seed = redis.pipeline();for (let i = 0; i < 1000; i++) seed.set(`b1:session:${i}`, '1');await seed.exec();
// SCAN in small batches: each call is cheap, the server stays responsive between callslet online = 0;for await (const batch of redis.scanStream({ match: 'b1:session:*', count: 200 })) { online += (batch as string[]).length;}console.log('online sessions:', online); // 1000
const cleanup = redis.pipeline();for (let i = 0; i < 1000; i++) cleanup.del(`b1:session:${i}`);await cleanup.exec();await redis.quit();พิมพ์ online sessions: 1000 (scanStream อาจให้ key เดียวกันซ้ำสองครั้งบน keyspace จริงที่เปลี่ยนแปลงอยู่ นั่นคือสัญญาของ SCAN และเป็นเหตุผลที่บท Scan Keys ทำ dedupe)
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”Redis คือ พจนานุกรมยักษ์เดียวที่ value มี type SET/GET/DEL/EXISTS/TYPE คือกริยาของพจนานุกรมเอง; value แต่ละ type มีกริยาของตัวเองเพิ่ม (RPUSH, HSET, …) เพราะพจนานุกรมคือ hash table ตัวเดียวและคำสั่งรันทีละตัว อะไรก็ตามที่เดินทั้งตาราง (KEYS) จึงบล็อกทุกคน ส่วนอะไรที่ตรงไปยัง key เดียว (GET) เป็น O(1) ไม่ว่า database จะใหญ่แค่ไหน
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| ตัวเลือก | ข้อดี | ข้อเสีย |
|---|---|---|
| โมเดล key-value แบบ schema-less | เริ่มง่าย — เก็บอะไรก็ได้ทันทีโดยไม่ต้องนิยามตารางก่อน | ไม่มี schema หรือการบังคับ type ฝั่งเซิร์ฟเวอร์; type ไม่ตรงกันจะโผล่ตอน runtime (WRONGTYPE) |
ใช้ KEYS * เพื่อดูทุกอย่าง | สะดวกเวลาทดลองบน dataset dev ขนาดเล็ก | บล็อก command thread ตลอดการสแกน: 156 ms ที่ 1M key และเพิ่มเป็นเส้นตรง ห้ามใช้บน dataset production ขนาดใหญ่ |
| เก็บ key โดยไม่มี TTL | ข้อมูลไม่หายเอง — ดีสำหรับข้อมูลที่ต้องคงอยู่ | ถ้าลืมตั้ง expiry กับข้อมูลที่ควรเป็น cache หน่วยความจำโตไม่จำกัด |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “
KEYSไม่เป็นไร database ผมเล็ก” ไม่เป็นไรจนกว่าจะเป็น และความล้มเหลวคือ latency spike ที่เงียบ ๆ ทำให้SCANเป็นนิสัย - “
DEL a bคืน 2” มันคืนจำนวน key ที่ถูกลบจริง - “key ของ Redis แยก namespace ตาม type” keyspace แบนหนึ่งเดียว:
user:42เป็นทั้ง hash และ string ไม่ได้ - “
OBJECT ENCODINGเป็นคุณสมบัติตายตัวของ type” มันเปลี่ยนตามขนาดและเนื้อหา (embstrเป็นraw,listpackเป็นhashtable)
Common mistakes
หัวข้อที่มีชื่อว่า “Common mistakes”- ใช้
KEYS *ใน production เพื่อดูทุก key — คำสั่งนี้บล็อกเซิร์ฟเวอร์จนสแกนเสร็จ บน dataset ใหญ่อาจทำให้ client อื่นทุกตัวค้าง ใช้SCANแทนเมื่อทำงานกับข้อมูลจริง - ไม่ตั้ง TTL ให้ key ที่ควรเป็นข้อมูลชั่วคราว — ถ้า
SETค่า cache โดยไม่มี expiry หน่วยความจำจะสะสมจนถึงขีดจำกัดmaxmemory - ทดสอบคำสั่งผ่าน
redis-cliแล้วคิดว่าพฤติกรรมในโค้ดแอปจริงเหมือนกันเป๊ะ — client library คืนnullแทน(nil), คืนตัวเลขแทน integer และ throw เมื่อเจอWRONGTYPEทดสอบผ่าน library จริงก่อนขึ้น production
Real-world example
Stack Overflow ใช้
SET/GETธรรมดาแบบนี้เป็นส่วนหนึ่งของ cache L1/L2 หน้า SQL Server ลด query ซ้ำสำหรับหน้าที่มีการอ่านพร้อมกันหนัก ที่ขนาดรายงานไว้ราว 160 พันล้านการดำเนินการต่อเดือน (Stack Overflow: The Architecture – 2016 Edition, Nick Craver, 2016 — อายุสิบปีแล้ว ถือเป็นประวัติศาสตร์)