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

คำสั่งแรกของคุณ

ทดสอบบน Redis 8.10.2 (redis:8.10.2-alpine), ioredis 6.0.0, Node 24 บน Mac (Apple silicon) transcript ด้านล่างเป็น session เดียวเรียงตามลำดับเอกสาร (ทุก fence ที่ขึ้นต้น # cli/... ถูก harness ของคอร์สรันซ้ำ) และตัวเลขเวลาวัดบนเครื่องนี้

โดยแก่นแล้ว 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 key value เก็บค่า GET key ดึงค่ากลับมา ถ้า key มีอยู่แล้ว SET จะเขียนทับ

cli/intro-first-commands-1.redis
127.0.0.1:6379> SET name "Redis"
OK
127.0.0.1:6379> GET name
"Redis"
127.0.0.1:6379> SET score 100
OK

EXISTS key คืน 1 ถ้ามี 0 ถ้าไม่มี ส่งหลาย key ได้ — ค่าที่คืนคือจำนวน key ที่มีอยู่ (key ที่ระบุซ้ำสองครั้งนับสองครั้ง)

cli/intro-first-commands-2.redis
127.0.0.1:6379> EXISTS name
(integer) 1
127.0.0.1:6379> EXISTS missing
(integer) 0
127.0.0.1:6379> EXISTS name score missing
(integer) 2

TYPE key คืน type ของ value ที่เก็บใน key: string, list, hash, set, zset หรือ stream และคืน none ถ้า key ไม่มีอยู่

cli/intro-first-commands-3.redis
127.0.0.1:6379> TYPE name
string
127.0.0.1:6379> TYPE missing
none

KEYS pattern คืนทุก key ที่ตรงกับ glob pattern KEYS * ตรงกับทุกอย่าง ลำดับเป็นอะไรก็ได้ที่ hash table ภายในให้มา — อย่าพึ่งพาลำดับนี้

cli/intro-first-commands-4.redis
127.0.0.1:6379> KEYS *
1) "score"
2) "name"

คำเตือน: ห้ามใช้ KEYS * ใน production การรันคำสั่งเป็น single-threaded — KEYS บล็อกเซิร์ฟเวอร์ทั้งตัวจนกว่าจะสแกนทุก key เสร็จ ใช้ SCAN แทน ซึ่งวนทีละ batch เล็ก ๆ หัวข้อถัดไปวัดว่ามันแพงเท่าไร

DEL key [key ...] ลบ key หนึ่งตัวหรือหลายตัว และคืน จำนวน key ที่ถูกลบจริง ถ้า key ไม่มีอยู่ก็แค่ข้าม และไม่นับรวมในค่าที่คืน

cli/intro-first-commands-5.redis
127.0.0.1:6379> DEL name
(integer) 1
127.0.0.1:6379> GET name
(nil)
127.0.0.1:6379> DEL name score
(integer) 1

DEL ตัวที่สองระบุ key ที่หายไปแล้วหนึ่งตัวกับที่ยังอยู่หนึ่งตัว จึงคืน 1 ไม่ใช่ 2

SET name "Redis"
GET name
SET score 100
EXISTS name
EXISTS missing
TYPE name
KEYS *
DEL name
GET name

หนึ่ง key ต่อหนึ่ง type แล้วใช้ TYPE และ OBJECT ENCODING กับแต่ละตัว (encoding คือวิธีที่ Redis จัดวางค่าในหน่วยความจำ — ค่าเล็กใช้ listpack ซึ่งเป็น array แบบกะทัดรัด และจะเห็นการสลับไปโครงสร้างใหญ่ขึ้นในบทถัด ๆ ไป):

cli/intro-first-commands-6.redis
127.0.0.1:6379> SET user:42:name "Ada"
OK
127.0.0.1:6379> RPUSH user:42:events login view
(integer) 2
127.0.0.1:6379> HSET user:42 name Ada plan pro
(integer) 2
127.0.0.1:6379> SADD user:42:tags admin beta
(integer) 2
127.0.0.1:6379> ZADD user:42:score 100 Ada
(integer) 1
127.0.0.1:6379> XADD user:42:log * action login
"1790747304943-0"
127.0.0.1:6379> TYPE user:42:events
list
127.0.0.1:6379> TYPE user:42
hash
127.0.0.1:6379> TYPE user:42:tags
set
127.0.0.1:6379> TYPE user:42:score
zset
127.0.0.1:6379> TYPE user:42:log
stream
127.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 บล็อกเซิร์ฟเวอร์” เป็นข้อกล่าวอ้างที่วัดได้ ผมโหลด 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,00015.8 ms—
1,000,000155.9 ms1.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 เอง

CLI ไว้สำรวจ; แอปใช้ client library คำสั่งเดียวกันผ่าน ioredis 6.0.0 — สังเกตว่าค่าที่คืนหน้าตาเป็นอย่างไรใน JavaScript:

src/intro-first-commands-basic.ts
// @run
import { 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 closes

output เมื่อรัน: Redis, null, 1, string, 1 key ที่ไม่มีคือ null, integer เป็น number และ quit() (ไม่ใช่ disconnect()) ปล่อยให้คำสั่งที่ค้างอยู่จบก่อน listener error สำคัญ: ถ้าไม่มี เซิร์ฟเวอร์ที่เข้าถึงไม่ได้จะทำให้เกิด event error ที่ไม่มีใครดักจับ

key เป็นแค่ string จึงตั้งชื่ออะไรก็ได้ — แต่ชื่อที่มีความหมายทำให้ debug และ monitor ง่ายขึ้นมาก convention มาตรฐานคือ namespace คั่นด้วย colon:

รูปแบบตัวอย่าง
object:iduser:42
object:id:fielduser:42:email
type:identifiersession:abc123

วิธีนี้ทำให้จัดกลุ่ม key ที่เกี่ยวข้อง ใช้ glob pattern อย่างปลอดภัย และเห็นได้ทันทีว่า key เก็บอะไรในเครื่องมืออย่าง RedisInsight

AI ถูกขอให้ทำ endpoint “ผู้ใช้ออนไลน์” แล้วเขียนว่า:

🐛 หา bug ในโค้ดนี้

app.get('/online', async (_req, res) => {
// count active sessions
const 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:

src/intro-first-commands-count.ts
// @run
import { 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 calls
let 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)

Redis คือ พจนานุกรมยักษ์เดียวที่ value มี type SET/GET/DEL/EXISTS/TYPE คือกริยาของพจนานุกรมเอง; value แต่ละ type มีกริยาของตัวเองเพิ่ม (RPUSH, HSET, …) เพราะพจนานุกรมคือ hash table ตัวเดียวและคำสั่งรันทีละตัว อะไรก็ตามที่เดินทั้งตาราง (KEYS) จึงบล็อกทุกคน ส่วนอะไรที่ตรงไปยัง key เดียว (GET) เป็น O(1) ไม่ว่า database จะใหญ่แค่ไหน

ตัวเลือกข้อดีข้อเสีย
โมเดล key-value แบบ schema-lessเริ่มง่าย — เก็บอะไรก็ได้ทันทีโดยไม่ต้องนิยามตารางก่อนไม่มี schema หรือการบังคับ type ฝั่งเซิร์ฟเวอร์; type ไม่ตรงกันจะโผล่ตอน runtime (WRONGTYPE)
ใช้ KEYS * เพื่อดูทุกอย่างสะดวกเวลาทดลองบน dataset dev ขนาดเล็กบล็อก command thread ตลอดการสแกน: 156 ms ที่ 1M key และเพิ่มเป็นเส้นตรง ห้ามใช้บน dataset production ขนาดใหญ่
เก็บ key โดยไม่มี TTLข้อมูลไม่หายเอง — ดีสำหรับข้อมูลที่ต้องคงอยู่ถ้าลืมตั้ง expiry กับข้อมูลที่ควรเป็น cache หน่วยความจำโตไม่จำกัด
  • “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)
  • ใช้ 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 — อายุสิบปีแล้ว ถือเป็นประวัติศาสตร์)

DEL คืนอะไร?
EXISTS คืนอะไรสำหรับ key ที่ไม่มีอยู่?
TYPE คืนอะไรสำหรับ key ที่เก็บ string ธรรมดา และสำหรับ key ที่ไม่มี?
ทำไมควรหลีกเลี่ยง KEYS * ใน production?
ใน ioredis await redis.get('missing') คืนอะไร?
รัน GET user:42:events บน key ที่เก็บ list จะเกิดอะไรขึ้น?