Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Redis

ความเข้าใจผิดทั่วไปเกี่ยวกับ Redis Streams: สิ่งที่คุณต้องรู้

ผมเองก็เคยอธิบาย Streams ผิด—ผมเคยนิยามมันว่า "ชุดขององค์ประกอบแบบ hashmap ที่เรียงลำดับตามเวลา ภายใต้คีย์เดียว" สิ่งนี้ไม่ถูกต้อง ส่วนสุดท้ายเกี่ยวกับเวลาและคีย์ถูกต้อง แตส่วนแรกผิดทั้งหมด

มาดูว่าไทำไม Streams ถูกเข้าใจผิดและมันทำงานจริงๆ อย่างไร เราจะประเมินจุดดีและจุดร้ายของความเข้าใจผิดนี้และว่ามันอาจส่งผลต่อซอฟต์แวร์ของคุณอย่างไร สุดท้ายเราจะตรวจสอบรูปแบบบางอย่างที่ไม่ชัดเจนที่ใช้ประโยชน์จากคุณสมบัติที่น้อยคนรู้จักของ Redis Streams

พื้นหลัง

ก่อนอื่น มาดูคำสั่ง XADD เนื่องจากนี่คือจุดเริ่มต้นของความเข้าใจผิด ลายเซ็นคำสั่งในเอกสารอย่างเป็นทางการของ redis.io มีลักษณะดังนี้:

XADD key ID field value [field value ...]

key อธิบายได้เอง id คือการรวม timestamp/sequence สำหรับรายการใหม่ แต่ในความเป็นจริง มันเกือบจะเป็น * เสมอเพื่อระบุการสร้างอัตโนมัติ รากเหง้าของความสับสนนี้เริ่มจาก field และ value หากคุณดูลายเซ็นคำสั่งสำหรับ HSET คุณจะเห็นรูปแบบที่คล้ายกันมาก:

HSET key field value [field value ...]

ลายเซ็นของสองคำสั่งนี้ต่างกันเพียงอาร์กิวเมนต์เดียว และอาร์กิวเมนต์นั้นใน XADD เกือบจะเป็น * เสมอ ดูคล้ายกันมาก ใช้術語เดียวกัน ต้องเหมือนกันใช่ไหม?

ตอนต่อปัญหา มาลองเว้น Redis ไปก่อน และดูว่าภาษาโปรแกรมมิ่งจัดการกับคู่ field-value อย่างไร สำหรับส่วนใหญ่ ไม่ว่าจะเป็นภาษาไหน วิธีที่เป็นตัวแทนที่สุดในการอธิบาย field-value คือด้วยเซต (ไม่ซ้ำ) ของฟิลด์ที่เชื่อมโยงกับค่า—บางภาษาจะรักษาลำดับของฟิลด์ บางภาษาไม่รักษา มาดูลึกขึ้นด้วยการเปรียบเทียบข้ามภาษาเล็กน้อย:

สิ่งนี้แมปได้ดีกับ Redis Hashes—ทั้งหมดนี้สามารถอธิบายคุณสมบัติของ hash ได้ ซึ่งไม่มีลำดับและไม่ซ้ำกัน PHP arrays, Python dicts, และ JavaScript maps สามารถกำหนดลำดับฟิลด์ได้ แต่หากคุณทำงานกับ hashes ใน Redis มันไม่สำคัญ คุณแค่ต้องรู้ว่าคุณไม่สามารถพึ่งพาลำดับนี้ในระดับแอปพลิเคชันได้

สรุปที่เป็นธรรมชาติสำหรับคนส่วนใหญ่คือ เนื่องจากลายเซ็นคำสั่งของ HSET และ XADD มีความสัมพันธ์ พวกเขาจึงอาจมีความสัมพันธ์คล้ายกันในการคืนค่า จริงๆ แล้วที่ระดับโปรโตคอลใน RESP2 ทั้งสองคืนค่าเป็น RESP Arrays แบบ interleaved นี้ต่อเนื่องในเวอร์ชันแรกของ RESP3 ที่ response ของ HGETALL และ XREAD ทั้งคู่เป็น maps (รายละเอียดเพิ่มเติมภายหลัง)

บั๊กหนึ่งทำให้ผมเปลี่ยนใจ

ปกติผมเขียนโค้ดด้วย JavaScript และบางครั้ง Python بصفتی ผู้ที่สื่อสารเกี่ยวกับ Redis ผมต้องเข้าถึงคนมากที่สุด และภาษาทั้งสองนี้เข้าใจง่าย ดังนั้นโค้ดตัวอย่างในภาษาใดภาษาหนึ่งจะถูกเข้าใจโดยเปอร์เซ็นต์สูงของนักพัฒนาโลก ไม่กี่วันก่อน ผโจมีโอกาสพูดที่งานประชุม PHP และต้องแปลงโค้ด Python ที่มีอยู่เป็น PHP ผมใช้ PHP ไปมาเกือบ 20 ปี แต่มันไม่เคยเป็นความหลงใหลของผม ตัวอย่างเฉพาะนั้นไม่ค่อยเหมาะกับการทำงานแบบ mod_php ดังนั้นผมใช้ swoole และการทำงานแบบ co-routine (โดยข้างเคียง การคุ้นเคยกับโลก JavaScript ทำให้ swoole ทำให้ PHP คุ้นเคยมากขึ้นสำหรับผม) ไลบรารีค่อนข้างเก่าและต้องส่งคำสั่ง Redis แบบดิบโดยไม่มีความช่วยเหลือจากไคลเอ็นต์ไลบรารีในการถอดรหัสผลลัพธ์ในระดับสูง โดยทั่วไป การส่งคำสั่ง Redis แบบดิบและถอดรหัสผลลัพธ์ให้การควบคุมมากกว่าและไม่ยุ่งยาก

ดังนั้น เมื่อส่งคำสั่งไปยัง XADD ผมสร้างส่วน field-value และมีข้อผิดพลาด off-by-one (ขโมยมาจากการกลับมาด้วย PHP หลายปี) ทำให้ผมส่งอย่างไม่ตั้งใจบางอย่างในลักษณะของ:

XADD myKey * field0 value1 field0 value2 field2 value3

แทนที่จะส่งฟิลด์และค่าที่สัมพันธ์กัน (field0 ถึง value0 และไปเรื่อยๆ)

ภายหลังในโค้ด ผมนำผลลัพธ์ของ XREAD ไปใส่ใน PHP array ที่มีอยู่แล้ว (ซึ่งเป็น associative) ในลูป กำหนดแต่ละฟิลด์เป็นคีย์กับแต่ละค่า และข้ามสิ่งที่ตั้งค่าไว้แล้ว ดังนั้นผมเริ่มต้นด้วย array แบบนี้:

array(1) { ["foo"]=> string(3) "bar" }

และจบลงด้วย array แบบนี้:

array(3) { ["foo"]=> string(3) "bar" ["field0"]=> string(6) "value1" ["field2"]=> string(6) "value3" }

ผมไม่ thểจินตนาการได้ว่ามันเป็นไปได้อย่างไร ผมค้นพบบั๊กอย่างรวดเร็วเกี่ยวกับว่าทำไม value1 ถูกกำหนดให้ field0 (ข้อผิดพลาด off-by-one ที่กล่าวมาข้างต้นใน XADD ของผม) แต่ทำไม field0 ไม่ได้ถูกตั้งค่าเป็น value2? ใน HSET พฤติกรรมนี้สำหรับการเพิ่มฟิลด์ในพื้นฐานคือ upsert—อัปเดตฟิลด์หากมีอยู่ มิฉะนั้นเพิ่มฟิลด์และตั้งค่า

ตรวจสอบ log MONITOR และเล่นค่าซ้ำ ผมรัน XREAD ดังนี้:

> XREAD STREAMS myKey 0
1) 1) "myKey"
2) 1) 1) "1592409586907-0"
2) 1) "field0"
2) "value1"
3) "field0"
4) "value2"
5) "field2"
6) "value3"

การซ้ำอยู่และถูกบันทึก ไม่ใช่ upserted นอกจากนี้ลำดับยังคงรักษาไว้ สิ่งนี้ไม่เหมือน hash เลย!

คิดถึงเรื่องนี้โดยใช้ JSON เป็นการประมาณ ผมคิดว่ารายการ Stream ควรจะเป็นแบบนี้:

{ "field1" : "value", "field2" : "value", "field3" : "value" }

แตจริงๆ แล้วมันเป็นแบบนี้:

[ ["field1", "value"], ["field2", "value"], ["field3", "value"] ]

สิ่งนี้หมายความว่าอะไร?

ข่าวดี

หากคุณมีโค้ดที่ทำงานกับ Streams อยู่แล้วและคุณสมมติว่ารายการเหมือน hash maps คุณน่าจะโอเค คุณแค่ต้องระวังบั๊กที่อาจเกิดขึ้นเกี่ยวกับการใส่ฟิลด์ซ้ำ เพราะมันอาจไม่ทำงาน כמוที่คุณคาดหวังในแอปพลิเคชันที่กำหนด นี่อาจไม่ใช้ได้ในทุกกรณีหรือไคลเอ็นต์ไลบรารี แต่แนวทางปฏิบัติที่ดีคือจัดเตรียมโครงสร้างข้อมูลที่ไม่อนุญาตให้ซ้ำ (ดูด้านบน) และ serialize เข้าสู่อาร์กิวเมนต์เมื่อจัดหาให้ XADD ฟิลด์ไม่ซ้ำเข้า ฟิลด์ไม่ซ้ำออก

ข่าวร้าย

ไม่ใช่ไคลเอ็นต์ไลบรารีและเครื่องมือทุกตัวทำถูกต้อง ไคลเอ็นต์ไลบรารีระดับต่ำ (ไม่ครบถ้วน: node_redis, hiredis) ไม่ทำอะไรมากนักในการเปลี่ยนเอาต์พุตจาก Redis ให้เป็นโครงสร้างภาษา ไคลเอ็นต์ไลบรารีระดับสูง บางตัว จัดการ abstraction ของการคืนค่าจริงของ Redis ให้เป็นโครงสร้างภาษา—คุณควรตรวจสอบว่าไลบรารีที่คุณเลือกทำอย่างนั้นหรือไม่ และสร้าง issue หากมันทำ บางไคลเอ็นต์ไลบรารีระดับสูงทำถูกต้องตั้งแต่แรก (stackexchange.redis) ดังนั้นจึงขอส่งขอบคุณ

ส่วนที่ค่อนข้างแย่อีกส่วน: หากคุณเป็นผู้ใช้ RESP3 ตั้งแต่แรกเริ่ม คุณอาจเคยประสบกับ XREAD / XREADGROUP คืนค่าเป็น RESP3 map type จนถึงต้นเดือนเมษายน เวอร์ชันพัฒนาของ Redis 6 ที่ยังไม่เสร็จสมบูรณ์ คืนค่า maps ที่ซ้ำกันเมื่ออ่าน Streams ขอบคุณที่แก้ไขแล้ว และเวอร์ชัน GA ของ Redis 6—ครั้งแรกที่คุณควรใช้ RESP3 ใน production—ส่งมอบการคืนค่าที่เหมาะสมสำหรับ XREAD / XREADGROUP

ส่วนสนุก

เพราะผมอธิบายไปแล้วว่าคุณอาจผิดเกี่ยวกับ Streams มาลองคิดดูว่าคุณสามารถใช้ประโยชน์จากโครงสร้างที่เข้าใจผิดมานี้ได้อย่างไร

นำความหมายเชิงความหมาย (semantic meaning) ไปใช้กับลำดับในรายการ Stream

ดังนั้น คุณจริงๆ แล้วมีเวกเตอร์สามอันให้เล่นในรูปแบบนี้ จินตนาการจัดเก็บเส้นทางสำหรับกราฟิกเวกเตอร์ แต่ละรายการ Stream จะเป็นรูปหลายเหลี่ยมหรือเส้นทางที่ไม่ซ้ำกัน และฟิลด์และค่าจะเป็นพิกัด ตัวอย่างแล้วใช้ส่วนของ SVG นี้:

<polyline points="50,150 50,200 200,200 200,100">

สิ่งนี้สามารถอธิบายได้ว่า:

> XADD mySVG * 50 150 50 200 200 200 200 100

รูปทรงเพิ่มเติมแต่ละรูปจะเป็นรายการอีกหนึ่งรายการบนคีย์เดียวกัน หากคุณพยายามทำอย่างนี้กับ Redis Hash คุณจะได้แค่สองพิกัดและไม่มีการรับประกันลำดับ อย่างที่ยอมรับ คุณอาจทำได้กับสิ่งต่างๆ เช่น bitfields แต่คุณจะสูญเสียความยืดหยุ่นมากเกี่ยวกับความยาวและขนาดพิกัด ด้วย Streams คุณอาจทำสิ่งสนุกบางอย่างกับ timestamp เพื่อแสดงชุดของรูปทรงที่ปรากฏตามเวลา

สร้างเซตของไอเท็มที่เรียงลำดับตามเวลา (time-ordered set of sequenced items)

สิ่งนี้ต้องการการแฮ็กเล็กน้อย แต่สามารถให้ฟังก์ชันมากมาย จินตนาการว่าคุณเก็บลำดับของข้อมูลแบบอาร์เรย์ อย่างมีประสิทธิภาพ อาร์เรย์ของอาร์เรย์—ใน JSON คุณอาจคิดว่ามันเป็นอย่างนี้:

[ ["A New Hope", "The Empire Strikes Back", "Return of the Jedi"], ["The Phantom Menace", "Attack of the Clones", "Revenge of the Sith"], ["The Force Awakens", "The Last Jedi", "The Rise of Skywalker"] ]

คุณสามารถอธิบายสิ่งนี้เป็นชุดของรายการ Stream พร้อมด้วยรายละเอียดเล็กน้อย: คุณต้องแน่ใจว่าจำนวน (pseudo) องค์ประกอบในรายการภายในของคุณไม่ใช่เลขคี่ หากเป็นเลขคี่ ตามข้างต้น คุณจะต้องบันทึกเรื่องนั้นไว้อย่างใดอย่างหนึ่ง—นี่คือวิธีที่ผมทำด้วยสตริงว่าง:

> XADD starwars * "A New Hope" "The Empire Strikes Back" "Return of the Jedi" "" "1592427370458-0"
> XADD starwars * "The Phantom Menace" "Attack of the Clones" "Revenge of the Sith" "" "1592427393492-0"
> XADD starwars * "The Force Awakens" "The Last Jedi" "The Rise of Skywalker" "" "1592427414475-0"
> XREAD streams starwars 0
1# "starwars" => 1) 1) "1592427370458-0" 2) 1) "A New Hope" 2) "The Empire Strikes Back" 3) "Return of the Jedi" 4) ""
2) 1) "1592427393492-0" 2) 1) "The Phantom Menace" 2) "Attack of the Clones" 3) "Revenge of the Sith" 4) ""
3) 1) "1592427414475-0" 2) 1) "The Force Awakens" 2) "The Last Jedi" 3) "The Rise of Skywalker" 4) ""

คุณได้ประโยชน์มากในรูปแบบนี้ โดยจ่ายค่าใช้จ่าย (น้อย) ที่ต้องกรองสตริงความยาว 0 ออก

Streams เป็นแคชการแบ่งหน้า (pagination cache)

เรื่องที่ซับซ้อนที่คุณเห็นบ่อยคือรายการไอเท็มบนไซต์ (อีคอมเมิร์ซ, บอร์ดข้อความ ฯลฯ) มักจะถูกแคช แต่ผมเคยเห็นคนปวดหัวพยายามหาวิธีที่ดีที่สุดในการแคชข้อมูลประเภทนี้—คุณแคชชุดผลลัพธ์ทั้งหมดลงใน sorted set แล้วแบ่งหน้าด้วย ZRANGE หรือคุณจัดเก็บหน้าเต็มที่คีย์ string? ทั้งสองวิธีมีจุดเด่นและจุดอ่อน

ซึ่งเป็นที่น่าสนใจ Streams ทำงานได้สำหรับเรื่องนี้ ตัวอย่างเช่น รายการอีคอมเมิร์ซ คุณมีชุดของไอเท็ม แต่ละตัวมี ID ไอเท็มเหล่านั้นแสดงในชุดที่จำกัดของการจัดเรียงที่มักมีการกลับด้าน (A-Z, Z-A, ราคาต่ำไปสูง, สูงไปต่ำ, คะแนนสูงสุดไปต่ำสุด, ต่ำสุดไปสูงสุด ฯลฯ)

เพื่อสร้างแบบจำลองข้อมูลประเภทนี้ใน Stream คุณจะกำหนด "chunk" size เฉพาะและทำให้มันเป็นรายการ ทำไมถึง chunks ไม่ใช่หน้าผลลัพธ์เต็มที่รายการ? เพราะมันช่วยให้คุณมีขนาดหน้าที่แตกต่างกันใน pagination (เช่น 10 ไอเท็มต่อหน้าสามารถทำจาก 2 chunks ของ 5 แต่ละอัน ในขณะที่ 25 ต่อหน้าจะเป็น 5 chunks ของ 5 แต่ละอัน) แต่ละรายการจะมีฟิลด์ที่แมปกับ product IDs และค่าจะเป็นข้อมูลผลิตภัณฑ์ ดูตัวอย่างที่ง่ายๆ ด้วยขนาด chunk ที่ต่ำมาก:

ความเข้าใจผิดทั่วไปเกี่ยวกับ Redis Streams: สิ่งที่คุณต้องรู้

เมื่อคุณต้องการดึงค่าที่แคชไว้ คุณจะรัน XRANGE ด้วยอาร์กิวเมนต์ COUNT ตั้งค่าเป็นจำนวน chunks ที่ประกอบเป็นหน้าผลลัพธ์สำหรับอินเตอร์เฟซของคุณ ดังนั้น หากคุณต้องการหน้าแรก 4 ไอเท็ม คุณจะรัน:

> XRANGE listcache - + COUNT 2
1) 1) "0-1"
2) 1) "123"
2) "{ \"Red Belt\", ... }"
3) "456"
4) "{ \"Yellow Belt\", ... }"
2) 1) "0-2"
2) 1) "789"
2) "{ \"Blue Belt\", ... }"
3) "012"
4) "{ \"Purple Belt\", ... }"

เพื่อรับหน้าที่สอง 4 ไอเท็ม คุณจะต้องให้ lower bound Stream ID เพิ่มขึ้น 1 ในกรณีของเรา lower bound จะเป็น 0-2

> XRANGE listcache 0-3 + COUNT 2
1) 1) "0-3"
2) 1) "345"
2) "{ \"Black Belt\", ... }"
3) "678"
4) "{ \"Red Boa\", ... }"
2) 1) "0-4"
2) 1) "901"
2) "{ \"Yellow Boa\", ... }"
3) "234"
4) "{ \"Green Belt\", ... }"

สิ่งนี้ให้ข้อได้เปรียบด้านความซับซ้อนทางคอมพิวตเทียบกับ Sorted Sets หรือ Lists เนื่องจาก XRANGE ในกรณีนี้เป็น O(1) แตมีบางสิ่งที่ต้องจดจำ:

  • XREVRANGE สามารถใช้สำหรับการกลับด้าน แต่สิ่งนี้จะกลับลำดับของ "chunks" เท่านั้น ภายในแต่ละ chunk คุณจะต้องกลับลำดับในลอจิกแอปพลิเคชันซึ่งควรจะง่ายดาย
  • การค้นหาส่วนต่างๆ ของรายการเป็น "ฟรี" หากคุณตั้งค่า Stream IDs แบบเชิงเส้นด้วยตนเอง ดังนั้น chunk 1 คือ Stream ID 0-1, chunk 2 คือ 0-2 และไปเรื่อยๆ โปรดทราบว่าคุณต้องเริ่มจาก 1 แทน 0 เนื่องจากคุณไม่สามารถเพิ่มรายการ Stream ที่ 0-0 ได้

เหมือนกับคีย์ใดๆ คุณสามารถใช้ expiry เพื่อจัดการว่าจะให้ Stream อยู่ได้นานแค่ไหน ตัวอย่างวิธีการทำสิ่งนี้อยู่ใน stream-row-cache

หวังว่าบทความนี้จะให้บริบทเพิ่มเติมเกี่ยวกับว่า Streams ทำงานจริงๆ อย่างไร และว่าคุณสามารถใช้ประโยชน์จากคุณสมบัติที่น้อยคนรู้จักของ Streams ในแอปพลิเคชันของคุณได้อย่างไร