Pattern Subscriptions
ทดสอบบน Redis 8.10.2 (
redis:8.10.2-alpine), ioredis 6.0.0, Node 24 บน Mac (Apple silicon) ทุกอย่างด้านล่างคือ output ของสคริปต์@runและ transcript แบบ# cli/...ที่รันซ้ำ พื้นฐานของ Pub/Sub (at-most-once, subscriber แบบ RESP2 เทียบ RESP3) อยู่ใน Pub/Sub
Pattern subscription
หัวข้อที่มีชื่อว่า “Pattern subscription”PSUBSCRIBE pattern [pattern ...] คือ SUBSCRIBE ที่ใช้ glob pattern แทนชื่อ channel ตรงตัว การ subscribe ครั้งเดียวจึงติดตาม channel ทั้งตระกูลได้ รวมถึงที่ยังไม่มีอยู่
| Pattern | ความหมาย |
|---|---|
* | ลำดับตัวอักษรใดๆ รวมถึงว่างเปล่า |
? | ตัวอักษรเดียวพอดี |
[abc], [a-c] | หนึ่งตัวอักษรจากเซ็ตหรือช่วง |
[^x] | หนึ่งตัวอักษรที่ไม่ใช่ x |
\* | * ตัวอักษรจริง |
ผม subscribe connection หนึ่งด้วย news.*, h?llo, log.[ew]*, raw.[^x] แล้ว publish ไปยัง 11 channel แต่ละแถวคือจำนวนการส่งที่ PUBLISH รายงาน:
| Channel ที่ publish | จำนวนที่ส่ง | เพราะ |
|---|---|---|
news.sports | 1 | match news.* |
news. | 1 | * match สตริงว่างด้วย |
news.a.b | 1 | . เป็นตัวอักษรธรรมดา * ครอบคลุมมัน |
newsflash | 0 | ไม่มี prefix news. |
hello, hallo | 1, 1 | ? คือตัวอักษรเดี่ยวใดก็ได้ |
heello | 0 | ? คือหนึ่งตัวพอดี |
log.error | 1 | [ew] = e หรือ w |
log.info | 0 | i ไม่อยู่ในเซ็ต |
raw.a, raw.x | 1, 0 | [^x] ยกเว้น x |
event pmessage มีสี่ส่วน
หัวข้อที่มีชื่อว่า “event pmessage มีสี่ส่วน”ข้อความที่มาทาง pattern ไม่ใช่ message แต่เป็น pmessage: ["pmessage", pattern, channel, payload] ใน ioredis สอง event มี signature ของ handler ต่างกัน และ publish ครั้งเดียวอาจทำให้เกิดทั้งสอง: ผม subscribe channel ตรงตัว news.tech ด้วย แล้ว publish ที่นั่น ซึ่ง match ทั้ง subscription ตรงตัวและ news.* PUBLISH คืน 2 และ client เห็นสอง event:
// @runimport { Redis } from 'ioredis';
const url = process.env.REDIS_URL!;const sub = new Redis(url);const pub = new Redis(url);const seen: string[] = [];
// 'message' has (channel, message); 'pmessage' has FOUR arguments: (pattern, channel, message)sub.on('message', (channel, message) => seen.push(`message ${channel} ${message}`));sub.on('pmessage', (pattern, channel, message) => seen.push(`pmessage ${pattern} ${channel} ${message}`));
console.log(await sub.psubscribe('news.*', 'h?llo', 'log.[ew]*', 'raw.[^x]')); // 4 patterns -> count 4console.log(await sub.subscribe('news.tech')); // + 1 literal channel -> count 5
// which channels does each pattern catch? PUBLISH returns the number of deliveriesfor (const ch of ['news.sports', 'news.', 'news.a.b', 'newsflash', 'hello', 'hallo', 'heello', 'log.error', 'log.info', 'raw.a', 'raw.x']) { console.log(ch.padEnd(12), await pub.publish(ch, 'x'));}// news.tech matches BOTH the pattern news.* and the literal subscription: two deliveries, two eventsconsole.log('news.tech'.padEnd(12), await pub.publish('news.tech', 'both'));
await new Promise((r) => setTimeout(r, 50));console.log(seen.filter((s) => s.includes('news.tech'))); // [ 'message news.tech both', 'pmessage news.* news.tech both' ]// a match that came only through a pattern fires 'pmessage' and NOT 'message'console.log(seen.filter((s) => s.includes('news.sports'))); // [ 'pmessage news.* news.sports x' ]
sub.disconnect();pub.disconnect();Output จริง (Mac เครื่องนี้):
45news.sports 1news. 1news.a.b 1newsflash 0hello 1hallo 1heello 0log.error 1log.info 0raw.a 1raw.x 0news.tech 2[ 'message news.tech both', 'pmessage news.* news.tech both' ][ 'pmessage news.* news.sports x' ]อีกสองข้อเท็จจริงจากตัวเลขเหล่านั้น ค่าที่ psubscribe/subscribe คืน (4 แล้ว 5) คือ จำนวน subscription ทั้งหมดบน connection รวม channel และ pattern และการ match ที่มาทาง pattern อย่างเดียวจะยิง pmessage และ ไม่ยิง message client ที่ฟังเฉพาะ message จะไม่เห็นอะไรเลย
PUBLISH news.sports "score update"
PUBLISH news.tech "new release"
PUBLISH weather.today "sunny"
PUBSUB NUMPATPUNSUBSCRIBE
หัวข้อที่มีชื่อว่า “PUNSUBSCRIBE”PUNSUBSCRIBE [pattern ...] ลบ pattern subscription โดยตอบหนึ่งครั้งต่อ pattern ด้วย ["punsubscribe", pattern, remaining] remaining นับ subscription ที่เหลือบน connection ทั้งหมด (channel และ pattern) ตามที่ 4/5 ข้างบนแสดง และ connection ออกจากโหมด subscribe เมื่อถึง 0 ถ้าไม่ใส่อาร์กิวเมนต์จะลบทุก pattern
pattern มีต้นทุนเท่าไร
หัวข้อที่มีชื่อว่า “pattern มีต้นทุนเท่าไร”ทุก PUBLISH ต้องถูกเทียบกับ ทุก pattern ที่ active ขณะที่ channel ตรงตัวเป็นการ lookup ใน hash ผมวัดค่าเฉลี่ยฝั่งเซิร์ฟเวอร์ของ PUBLISH orders:1 x แบบ pipeline 20,000 ครั้งจาก INFO commandstats โดย subscriber มี pattern ที่ไม่มีวัน match:
// @runimport { Redis } from 'ioredis';
const url = process.env.REDIS_URL!;const pub = new Redis(url);
// average server time of PUBLISH over 20,000 publishes, from INFO commandstatsasync function usPerPublish(label: string) { await pub.config('RESETSTAT'); const batch = pub.pipeline(); for (let i = 0; i < 20000; i++) batch.publish('orders:1', 'x'); await batch.exec(); const line = (await pub.info('commandstats')).split('\r\n').find((l) => l.startsWith('cmdstat_publish')); console.log(label.padEnd(24), line?.match(/usec_per_call=([\d.]+)/)?.[1], 'us per PUBLISH');}
await usPerPublish('no subscriptions');for (const n of [100, 10000]) { const sub = new Redis(url); await sub.psubscribe(...Array.from({ length: n }, (_, i) => `p${i}:*`)); // none of them match orders:1 await usPerPublish(`${n} patterns`); sub.disconnect(); await new Promise((r) => setTimeout(r, 300));}const literal = new Redis(url);await literal.subscribe(...Array.from({ length: 10000 }, (_, i) => `c${i}`));await usPerPublish('10000 literal channels');literal.disconnect();pub.disconnect();Output จริง (Mac เครื่องนี้):
no subscriptions 0.04 us per PUBLISH100 patterns 0.77 us per PUBLISH10000 patterns 115.57 us per PUBLISH10000 literal channels 0.09 us per PUBLISH100 pattern จึงแพงกว่าฐานราว 20 เท่า (ยังต่ำกว่าหนึ่งไมโครวินาที), 10,000 pattern แพงกว่าราว 2,900 เท่า คือราว 0.1 ms ของ command thread ต่อทุกการ publish สำหรับ ทุก publisher; channel ตรงตัว 10,000 ตัวไม่มีต้นทุนที่วัดได้ ต้นทุนเป็นต่อ pattern และจ่ายบนเส้นทาง publish จึงควรมี pattern น้อยและแคบ และใช้ channel ตรงตัวเมื่อรู้ชื่อ
Keyspace notifications (teaser)
หัวข้อที่มีชื่อว่า “Keyspace notifications (teaser)”Redis publish event ภายในบน channel ในตัว ซึ่งเป็นสิ่งที่ pattern subscription ถูกใช้บ่อยที่สุด: __keyevent@0__:expired (key ใน db 0 หมดอายุ) และ __keyspace@0__:mykey (เกิดอะไรบางอย่างกับ mykey) ปิดอยู่เป็นค่าเริ่มต้น CONFIG GET รายงาน flag ตามลำดับมาตรฐาน KEA จึงกลับมาเป็น AKE:
127.0.0.1:6379> CONFIG SET notify-keyspace-events KEAOK127.0.0.1:6379> CONFIG GET notify-keyspace-events1) "notify-keyspace-events"2) "AKE"127.0.0.1:6379> CONFIG SET notify-keyspace-events ""OKบทเต็มพร้อม event expired ที่จับเวลา คือ Keyspace notifications
Reading AI-generated code
หัวข้อที่มีชื่อว่า “Reading AI-generated code”AI เขียน subscriber สำหรับทุก channel ของ alert:
🐛 หา bug ในโค้ดนี้
const sub = new Redis(process.env.REDIS_URL!);await sub.psubscribe('alerts.*');sub.on('message', (channel, message) => {notifyOncall(channel, message); // never runs});เฉลย
subscription สำเร็จและ PUBLISH alerts.billing ... รายงานว่าส่ง 1 แต่ handler ผูกกับ message; pattern subscription ปล่อย pmessage ที่มีอาร์กิวเมนต์แรกเพิ่ม จึงไม่มีใครถูกแจ้งเลย ในการรันของเรา การ match ผ่าน pattern อย่างเดียวให้ [ 'pmessage news.* news.sports x' ] และไม่มี event message เลย วิธีแก้คือ event และ signature ที่ถูก:
sub.on('pmessage', (pattern, channel, message) => { notifyOncall(channel, message);});Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”pattern subscription คือ ตัวกรองที่เซิร์ฟเวอร์ประเมินให้ทุกข้อความ มันซื้อความสามารถที่ไม่ต้องรู้ชื่อ channel ล่วงหน้า และคุณจ่ายด้วย CPU ทุกครั้งที่ publish บน command thread เดียว ถือว่า pattern เป็นทรัพยากรที่หายาก และถือว่า pmessage เป็น event แยกที่มีอาร์กิวเมนต์ของตัวเอง
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| ตัวเลือก | ข้อดี | ข้อเสีย |
|---|---|---|
PSUBSCRIBE พร้อม glob | subscription เดียวสำหรับทั้งตระกูล รวม channel ในอนาคต | ถูกเทียบกับทุกการ publish: 0.04 us ถึง 115 us ต่อการ publish จาก 0 เป็น 10,000 pattern |
SUBSCRIBE channel ตรงตัว | lookup ใน hash ต้นทุนคงที่แม้ 10,000 channel (0.09 us) | ต้องระบุชื่อเอง |
| Keyspace notification ผ่าน Pub/Sub | event แบบ real-time โดยไม่ต้อง poll | งานเพิ่มกับคำสั่งที่แตะ key และยังเป็นส่งแล้วลืม |
| subscribe channel ตรงตัวที่ pattern ครอบคลุมอยู่แล้ว | บางครั้งตั้งใจ | ข้อความเดียวกันถูกส่งสองครั้ง (PUBLISH คืน 2) |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “การ match ของ pattern มาถึงเป็น
message” มันมาถึงเป็นpmessageสี่ส่วน - “ตัวเลขที่
PSUBSCRIBEคืนนับ pattern” มันนับทุก subscription บน connection รวม channel ด้วย - “pattern ฟรี” มันเป็นเชิงเส้นตามจำนวน pattern ที่ active: 115 us ต่อการ publish ที่ 10,000
- “
*ต้องมีอย่างน้อยหนึ่งตัวอักษร”news.*matchnews.ได้
Common mistakes
หัวข้อที่มีชื่อว่า “Common mistakes”- ฟัง
messageบน pattern subscription — ผูกpmessage(ดูแบบฝึกหัด) - subscribe
*หรือ pattern เป็นพัน — ทุกการ publish จ่ายให้ทุก pattern - เปิด
KEA(ทุกอย่าง) บน production ที่วุ่นวาย — เปิดเฉพาะ class ของ event ที่คุณใช้
Real-world example
เอกสารของ Redis เอง อธิบาย pattern subscription แบบ glob, รูปแบบ
pmessageและความหมายของตัวนับ subscription ใน Redis Pub/Sub; กฎของ pattern ก็คือ glob เดียวกับKEYS(PSUBSCRIBE)