Sets
ทดสอบบน Redis 8.10.2 (
redis:8.10.2-alpine), ioredis 6.0.0, Node 24 บน Mac (Apple silicon) transcript แบบ# cli/...คือ session เดียวเรียงตามเอกสารที่ harness ของคอร์สรันซ้ำ ตัวเลขเวลาเป็นไมโครวินาทีฝั่งเซิร์ฟเวอร์จากSLOWLOG(threshold 0) บนเครื่องนี้ ถือเป็นระดับขนาด ไม่ใช่ benchmark
Redis Set คืออะไร?
หัวข้อที่มีชื่อว่า “Redis Set คืออะไร?”Redis Set คือคอลเลกชันของ string ที่ ไม่ซ้ำกัน และ ไม่มีลำดับที่รับประกัน การเพิ่ม member ที่มีอยู่แล้วไม่ทำอะไร จึงไม่เกิดข้อมูลซ้ำ ทำให้ Set เหมาะกับ tag, การเช็ก “เคยเห็นแล้ว” และกลุ่มใดๆ ที่แต่ละค่าปรากฏได้ครั้งเดียว
SADD และ SREM — เพิ่มและลบ member
หัวข้อที่มีชื่อว่า “SADD และ SREM — เพิ่มและลบ member”SADD key member [member ...] เพิ่ม member หนึ่งตัวขึ้นไปและคืนจำนวน member ที่ ใหม่ (ตัวซ้ำไม่นับ) SREM key member [member ...] ลบ member และคืนจำนวนที่มีอยู่จริง
127.0.0.1:6379> SADD article:1:tags "redis" "database" "caching"(integer) 3127.0.0.1:6379> SADD article:1:tags "redis"(integer) 0127.0.0.1:6379> SREM article:1:tags "caching"(integer) 1127.0.0.1:6379> SREM article:1:tags "nosql"(integer) 0127.0.0.1:6379> SCARD article:1:tags(integer) 2127.0.0.1:6379> OBJECT ENCODING article:1:tags"listpack"SCARD คืนจำนวน member โดยไม่ต้องดึงมา สังเกตบรรทัดสุดท้าย: set เล็กของ string ถูกเก็บเป็น listpack ไม่ใช่ hash table หัวข้อถัดไปแสดงว่ามีผลต่อลำดับอย่างไร
SADD article:1:tags "redis" "database" "caching"
SADD article:1:tags "redis"
SREM article:1:tags "caching"
SREM article:1:tags "nosql"สาม encoding และลำดับที่ห้ามพึ่งพา
หัวข้อที่มีชื่อว่า “สาม encoding และลำดับที่ห้ามพึ่งพา”set เลือก encoding ภายในหนึ่งในสามแบบตามสิ่งที่มันเก็บ ลำดับที่ SMEMBERS คืนมาเป็นผลของ encoding และสิ่งเดียวที่เป็นสัญญาคือ ไม่มีลำดับที่รับประกัน:
127.0.0.1:6379> SADD s z a m(integer) 3127.0.0.1:6379> SMEMBERS s1) "z"2) "a"3) "m"127.0.0.1:6379> SADD n 30 10 20(integer) 3127.0.0.1:6379> SMEMBERS n1) "10"2) "20"3) "30"127.0.0.1:6379> OBJECT ENCODING n"intset"127.0.0.1:6379> SADD n x(integer) 1127.0.0.1:6379> OBJECT ENCODING n"listpack"127.0.0.1:6379> SMEMBERS n1) "10"2) "20"3) "30"4) "x"| Encoding | เมื่อไหร่ (ค่าเริ่มต้น 8.10.2) | สิ่งที่เห็น |
|---|---|---|
intset | เฉพาะจำนวนเต็ม ไม่เกิน set-max-intset-entries (512) | 30 10 20 กลับมาเรียงแล้ว: 10 20 30 |
listpack | เล็กและผสมชนิด ไม่เกิน set-max-listpack-entries (128) | z a m กลับมาตามลำดับที่ใส่ |
hashtable | ใหญ่กว่านั้น | set 600 member เป็น hashtable |
ลำดับทั้งสองแบบเป็นรายละเอียดของ implementation เวอร์ชันนี้ ไม่ใช่คำสัญญา: ทันทีที่ set โตเป็น hashtable ลำดับก็ไม่เกี่ยวกับลำดับที่ใส่ อะไรที่ต้องการลำดับให้ใช้ List หรือ Sorted Set (บทเวอร์ชันก่อนแสดงลำดับ SMEMBERS ที่แต่งขึ้น; transcript ในบทนี้คือ output จริง)
SMEMBERS และ SCARD — ตรวจดู Set
หัวข้อที่มีชื่อว่า “SMEMBERS และ SCARD — ตรวจดู Set”SMEMBERS key คืนทุก member; SCARD key คืนจำนวน SMEMBERS เป็น O(N) ใช้ได้กับ set เล็ก ไม่ใช่กับ set ใหญ่ (วัดด้านล่าง)
SADD user:42:interests "music" "hiking" "redis" "coffee"
SMEMBERS user:42:interests
SCARD user:42:interestsSISMEMBER และ SMISMEMBER — เช็กการเป็นสมาชิก
หัวข้อที่มีชื่อว่า “SISMEMBER และ SMISMEMBER — เช็กการเป็นสมาชิก”SISMEMBER key member คืน 1 หรือ 0 แบบ O(1) ไม่ว่า set จะใหญ่แค่ไหน SMISMEMBER key member [member ...] (6.2+) เช็กหลาย member ใน round trip เดียว และคืน flag หนึ่งตัวต่อ member ตามลำดับ:
127.0.0.1:6379> SISMEMBER n 10(integer) 1127.0.0.1:6379> SMISMEMBER n 10 99 x1) (integer) 12) (integer) 03) (integer) 1SADD seen:urls "https://redis.io" "https://astro.build"
SISMEMBER seen:urls "https://redis.io"
SISMEMBER seen:urls "https://example.com"
SMISMEMBER seen:urls "https://redis.io" "https://example.com"SRANDMEMBER และ SPOP — เข้าถึงแบบสุ่ม
หัวข้อที่มีชื่อว่า “SRANDMEMBER และ SPOP — เข้าถึงแบบสุ่ม”SRANDMEMBER key [count] คืน member แบบสุ่ม โดยไม่ลบ; count ติดลบ อนุญาตให้ซ้ำและคืนครบ |count| รายการเสมอ SPOP key [count] ลบและคืน member แบบสุ่ม
127.0.0.1:6379> SRANDMEMBER n 21) "10"2) "20"127.0.0.1:6379> SRANDMEMBER n -51) "30"2) "x"3) "30"4) "10"5) "x"127.0.0.1:6379> SPOP n 11) "x"127.0.0.1:6379> SCARD n(integer) 3ผลเป็นการสุ่ม output ของคุณจึงต่างออกไป ได้ 5 รายการจาก set 4 member เพราะ count ติดลบ (30 และ x ซ้ำ)
SADD prize:pool "alpha" "beta" "gamma" "delta" "epsilon"
SRANDMEMBER prize:pool 2
SCARD prize:pool
SPOP prize:pool 1
SCARD prize:poolSINTERCARD — นับ intersection โดยไม่ต้องสร้างมัน (7.0+)
หัวข้อที่มีชื่อว่า “SINTERCARD — นับ intersection โดยไม่ต้องสร้างมัน (7.0+)”SINTERCARD numkeys key [key ...] [LIMIT n] คืนขนาดของ intersection และหยุดเร็วเมื่อถึง LIMIT เรื่องพีชคณิตของ set ทั้งหมดอยู่ใน Set operations
127.0.0.1:6379> SADD a 1 2 3 4 5(integer) 5127.0.0.1:6379> SADD b 3 4 5 6(integer) 4127.0.0.1:6379> SINTERCARD 2 a b(integer) 3127.0.0.1:6379> SINTERCARD 2 a b LIMIT 2(integer) 2SSCAN — เดินผ่าน set ใหญ่โดยไม่บล็อก
หัวข้อที่มีชื่อว่า “SSCAN — เดินผ่าน set ใหญ่โดยไม่บล็อก”ผมสร้าง set 200,000 member (user:1 .. user:200000, hashtable, 8.8 MB) แล้วถาม SLOWLOG ว่าแต่ละคำสั่งกินเซิร์ฟเวอร์เท่าไร:
| คำสั่ง | เวลาฝั่งเซิร์ฟเวอร์ | Reply |
|---|---|---|
SISMEMBER big user:150000 | 5 µs | integer ตัวเดียว |
SSCAN big 0 COUNT 100 | 44 µs | cursor + ราว 100 member |
SMEMBERS big | 11,116 µs (11 ms) | 200,000 member (ข้อความ member 2.1 MB) |
SMEMBERS ทำให้ command thread ค้าง 11 ms และท่วม client; SSCAN คืนทีละหน้า บน set เล็ก SSCAN คืนทุกอย่างในการเรียกเดียว:
127.0.0.1:6379> SADD big m1 m2 m3(integer) 3127.0.0.1:6379> SSCAN big 0 MATCH m* COUNT 201) "0"2) 1) "m1" 2) "m2" 3) "m3"cursor เป็น 0 ใน reply หมายความว่าสแกนเสร็จ เหมือน SCAN, SSCAN อาจคืน member เดิมซ้ำได้ และ COUNT เป็นคำใบ้ ไม่ใช่ขนาดหน้า
จาก Node.js: tag, dedupe และเดิน cursor
หัวข้อที่มีชื่อว่า “จาก Node.js: tag, dedupe และเดิน cursor”sadd คืนจำนวน member ใหม่ จึงใช้เป็น “เพิ่งเคยเห็นครั้งแรกไหม?” ได้ sscanStream ห่อ loop ของ cursor ไว้ให้:
// @runimport { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);await redis.del('b2:post:1:tags', 'b2:seen');
// SADD returns how many members were NEW, so it doubles as "first time seen?"console.log(await redis.sadd('b2:post:1:tags', 'redis', 'cache', 'redis')); // 2 (the duplicate is ignored)console.log(await redis.sismember('b2:post:1:tags', 'redis')); // 1console.log(await redis.smismember('b2:post:1:tags', 'redis', 'sql')); // [ 1, 0 ]
// dedupe: process an event only the first time its id is addedasync function firstTime(id: string) { return (await redis.sadd('b2:seen', id)) === 1;}console.log(await firstTime('evt-1'), await firstTime('evt-1')); // true false
// iterate a big set with a cursor instead of SMEMBERSfor (let i = 0; i < 1000; i++) await redis.sadd('b2:seen', `evt-${i}`);let n = 0;for await (const batch of redis.sscanStream('b2:seen', { count: 200 })) n += (batch as string[]).length;console.log('scanned', n, 'members'); // 1000 (evt-1 was already there)
await redis.del('b2:post:1:tags', 'b2:seen');await redis.quit();Output จริง: 2, 1, [ 1, 0 ], true false, scanned 1000 members
Reading AI-generated code
หัวข้อที่มีชื่อว่า “Reading AI-generated code”AI เขียนการเช็ก “ผู้ใช้ติดตาม tag X ไหม?” สำหรับ set ที่เก็บทุก tag ที่แพลตฟอร์มเคยเห็น:
🐛 หา bug ในโค้ดนี้
async function hasTag(tag: string) {const tags = await redis.smembers('tags:all'); // 200,000 membersreturn tags.includes(tag);}เฉลย
ทำงานได้ แต่ดาวน์โหลด set ทั้งก้อนเพื่อตอบคำถามใช่/ไม่ใช่ บน set 200,000 member SMEMBERS กินเซิร์ฟเวอร์ 11,116 µs และส่งข้อความ member 2.1 MB ต่อครั้ง เทียบกับ 5 µs และ integer ตัวเดียวของ SISMEMBER ส่วน includes ของ array ก็เป็น O(N) ฝั่ง client อีกชั้น จงถามเซิร์ฟเวอร์ในสิ่งที่คุณอยากรู้จริงๆ:
async function hasTag(tag: string) { return (await redis.sismember('tags:all', tag)) === 1;}sets-sorted-sets-sets-tags.ts ที่รันได้ข้างบนใช้ sismember/smismember และ sscanStream สำหรับกรณีที่ต้องการทุก member จริงๆ
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”Set คือ ถุงที่มีการทดสอบสมาชิก: “X อยู่ในนั้นไหม?” ใช้เวลาคงที่ไม่ว่าขนาดเท่าไร “ขอทั้งหมด” คิดต้นทุนตามขนาด และ “เรียงอย่างไร?” ไม่มีคำตอบ ออกแบบรอบการทดสอบ (SISMEMBER, ค่าที่ SADD คืน) ไม่ใช่รอบการเทเนื้อหาออกมา
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| ตัวเลือก | ข้อดี | ข้อเสีย |
|---|---|---|
| Set สำหรับ tag / dedupe | ไม่ซ้ำโดยไม่ต้องเช็กก่อน; SADD เป็น upsert ในตัวและบอกว่าเป็นของใหม่หรือไม่ | ไม่มีลำดับ (สิ่งที่เห็นเป็นผลของ encoding); ถ้าต้องเรียงหรือจัดอันดับให้ใช้ Sorted Set |
SISMEMBER / SMISMEMBER | O(1) ต่อ member, รวมเป็น round trip เดียว | ได้แค่ ใช่/ไม่ใช่: ข้อมูลของ member อยู่ใน key อื่น |
SMEMBERS | เรียกครั้งเดียวได้ทุกอย่าง | เซิร์ฟเวอร์ค้าง 11 ms และข้อความ 2.1 MB ที่ 200k member; ใช้ SSCAN |
SPOP สำหรับจับสุ่ม | สุ่มและลบแบบ atomic | member หายไปเลย; บันทึกการจับแยกถ้าต้องการประวัติ |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “
SMEMBERSคืน member ในลำดับคงที่” set เล็กเป็นลำดับที่ใส่ จำนวนเต็มเป็นลำดับเรียง set ใหญ่แทบเป็นลำดับใดก็ได้ ไม่มีอะไรรับประกัน - “
SADDคืนขนาดของ set” คืนจำนวน member ที่ใหม่ ใช้SCARDสำหรับขนาด - “
SRANDMEMBERพร้อม count คืน member ที่ไม่ซ้ำตามจำนวนนั้น” เฉพาะ count บวกที่ไม่ซ้ำ count ติดลบอนุญาตให้ซ้ำและคืนครบ|count|รายการ - “
SSCANคืนแต่ละ member ครั้งเดียวเป๊ะ” อาจคืน member ซ้ำได้ รับประกันเพียงว่า member ที่อยู่ตลอดการสแกนจะปรากฏ
Common mistakes
หัวข้อที่มีชื่อว่า “Common mistakes”- เรียก
SMEMBERSบน set ใหญ่ใน request path — ใช้SISMEMBERสำหรับคำถาม และSSCANสำหรับการเดิน - เช็กสมาชิกทีละคำสั่ง —
SMISMEMBER(หรือ pipeline) เปลี่ยน N round trip เป็นหนึ่ง - ใช้ Set ทั้งที่ต้องการลำดับ — ใช้ List หรือ Sorted Set ที่มี timestamp เป็น score
Real-world example
เอกสารของ Redis เอง ระบุการติด tag และ “รายการไม่ซ้ำ” เป็นการใช้งานมาตรฐานของ Set และอธิบายสาม encoding กับขีดจำกัดใน หน้า Sets data type