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

อธิบายการสแกนตาราง Redis Hash:ภายในกลไก

อธิบายการสแกนตาราง Redis Hash:ภายในกลไก

โดย เอฮุด ตามีร์

ความท้าทายใหญ่ประการหนึ่งสำหรับฉันในฐานะนักพัฒนาซอฟต์แวร์คือการอ่านโค้ดของผู้อื่น สำหรับโพสต์นี้ ฉันได้อ่านโค้ด C ที่น่าสนใจซึ่งฉันไม่เคยรู้มาก่อน และฉันกำลังจะนำเสนอให้คุณทราบ โค้ดที่ฉันจะพูดถึงเป็นส่วนหนึ่งของ Redis ฐานข้อมูลและสามารถพบได้ที่นี่

Redis เป็นฐานข้อมูลคีย์-ค่า ทุกรายการในฐานข้อมูลเป็นการแมปจากคีย์ไปยังค่า ค่าสามารถมีได้หลายประเภท มีทั้งจำนวนเต็ม รายการ ตารางแฮช และอื่นๆ เบื้องหลังฐานข้อมูลเองก็เป็นตารางแฮชเช่นกัน ในโพสต์นี้ เราจะมาสำรวจคำสั่ง SCAN ใน Redis

อธิบายการสแกนตาราง Redis Hash:ภายในกลไก _By © ผู้ใช้:Colin / Wikimedia Commons, CC BY-SA 4.0, [https://commons.wikimedia.org/w/index.php?curid=30343877](https://commons.wikimedia.org/w/index.php?curid=30343877" rel="noopener" target="blank" title=")

สแกน Redis

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

คำสั่ง SCAN มีคุณสมบัติที่น่าสนใจบางประการ:

  1. รับประกันว่ารายการทั้งหมดที่มีอยู่ในตารางจะถูกส่งคืน อย่างน้อยหนึ่งครั้ง .
  2. มัน ไร้สัญชาติ . ตารางจะไม่บันทึกข้อมูลใดๆ เกี่ยวกับเครื่องสแกนที่ใช้งานอยู่ นอกจากนี้ยังหมายความว่าการสแกนไม่ได้ล็อคฐานข้อมูล
  3. มีความยืดหยุ่นในการปรับขนาดตาราง เพื่อรักษาเวลาในการเข้าถึง O(1) ตารางแฮชจะคงค่าโหลดแฟคเตอร์ไว้ ตัวประกอบภาระจะวัดว่าโต๊ะ "เต็ม" แค่ไหนในเวลาที่กำหนด เมื่อปัจจัยในการรับน้ำหนักมีขนาดใหญ่เกินไปหรือเล็กเกินไป ตารางจะถูกปรับขนาด SCAN จะรักษาการรับประกันแม้ว่าจะถูกเรียกในขณะที่ตารางกำลังปรับขนาดก็ตาม

การใช้งาน

SCAN ถูกนำมาใช้ใน dict.c ในฟังก์ชัน 03 . นี่คือลายเซ็นของฟังก์ชันและการดูแลเพิ่มเติม:

unsigned long dictScan(dict *d,
 unsigned long v,
 dictScanFunction *fn,
 dictScanBucketFunction* bucketfn,
 void *privdata)
{
 dictht *t0, *t1;
 const dictEntry *de, *next;
 unsigned long m0, m1;
 if (dictSize(d) == 0) return 0;
 // ...

สิ่งที่น่าสังเกต:

  • ฟังก์ชันยอมรับ 5 พารามิเตอร์:12 พจนานุกรมที่จะสแกน 20 เคอร์เซอร์ และพารามิเตอร์อื่นๆ อีก 3 รายการที่เราจะมาพูดถึงในภายหลัง
  • ฟังก์ชันส่งคืนค่าเคอร์เซอร์ที่จะใช้ในการเรียกฟังก์ชันนี้ครั้งถัดไป หากฟังก์ชันคืนค่า 0 แสดงว่าการสแกนเสร็จสิ้น
  • 37 . เมื่อพจนานุกรมว่างเปล่า ฟังก์ชันจะส่งกลับ 0 เพื่อระบุว่าการสแกนเสร็จสิ้น

1. การสแกนปกติ

รหัสต่อไปนี้จะสแกนองค์ประกอบต่างๆ:

if (!dictIsRehashing(d)) {
 t0 = &(d->ht[0]);
 m0 = t0->sizemask;
 /* Emit entries at cursor */
 if (bucketfn) bucketfn(privdata, &t0->table[v & m0]);
 de = t0->table[v & m0];
 while (de) {
 next = de->next;
 fn(privdata, de);
 de = next;
 }
 /* Set unmasked bits so incrementing the reversed cursor
 * operates on the masked bits */
 v |= ~m0;
 /* Increment the reverse cursor */
 v = rev(v);
 v++;
 v = rev(v);
} else {
 // ...

มาดูกันทีละขั้นตอน เริ่มจากบรรทัดแรกด้านล่าง:

if (!dictIsRehashing(d)) {
 t0 = &(d->ht[0]);
 m0 = t0->sizemask;

การปรับปรุงใหม่คือกระบวนการกระจายองค์ประกอบให้เท่าๆ กันทั่วทั้งตารางหลังจากปรับขนาดแล้ว ตารางแฮช dict.c แฮชใหม่ แบบค่อยเป็นค่อยไป ซึ่งหมายความว่าจะไม่ทำการปรับปรุงทั้งตารางในคราวเดียว แต่จะทำทีละน้อย การดำเนินการทุกอย่างที่ทำบนโต๊ะ เช่น เพิ่ม ลบ ค้นหา รวมถึงดำเนินการขั้นตอนการปรับปรุงใหม่ด้วย ซึ่งจะทำให้ตารางพร้อมใช้งานสำหรับการดำเนินการในระหว่างการทำการปรับปรุงใหม่ เนื่องจากวิธีการทำการปรับปรุงใหม่ ฟังก์ชันจึงทำงานแตกต่างออกไปในระหว่างการทำการปรับปรุงใหม่ เราจะเริ่มต้นด้วยการดูว่าจะเกิดอะไรขึ้นเมื่อตารางไม่ได้ทำการปรับปรุงใหม่

ตัวชี้ไปยังตารางแฮชจะถูกบันทึกไว้ในตัวแปรท้องถิ่น 43 และมาสก์ขนาด ถูกบันทึกไว้ใน 57 . มาสก์ขนาด: ตารางแฮช dict.c จะเป็น 69 เสมอ ในขนาด สำหรับขนาดตารางที่กำหนด หน้ากากขนาดคือ 72 ซึ่งเป็นเลขฐานสองที่มี 88 บิตที่มีนัยสำคัญน้อยที่สุดตั้งค่าเป็น 1 ตัวอย่างเช่น สำหรับ 92 . สำหรับคีย์ที่กำหนด ตำแหน่งของคีย์ในตารางแฮชจะเป็น 109 สุดท้าย บิตของแฮช ของกุญแจ เราจะเห็นการดำเนินการนี้ในอีกสักครู่

ตารางแฮช dict.c ใช้การกำหนดที่อยู่แบบเปิด ทุกรายการในตารางเป็นรายการเชื่อมโยงขององค์ประกอบที่มีค่าแฮชที่ขัดแย้งกัน สิ่งนี้เรียกว่า ที่เก็บข้อมูล . ในส่วนถัดไปนี้ ที่เก็บข้อมูล ขององค์ประกอบถูกสแกน:

/* Emit entries at cursor */
if (bucketfn) bucketfn(privdata, &t0->table[v & m0]);
de = t0->table[v & m0];
while (de) {
 next = de->next;
 fn(privdata, de);
 de = next;
}

สังเกตการใช้มาสก์ขนาด :110 m0125 . v อาจอยู่นอกช่วงที่จัดทำดัชนีได้ของ tabl133 m0 ใช้มาสก์ขนาดเพื่อเก็บเฉพาะ la142 ไม่มีหลัก 151 f v และให้ดัชนีที่ถูกต้องลงในตาราง

ตอนนี้คุณอาจเดาได้ว่า 164 คืออะไร มีไว้เพื่อ 174 จัดทำโดยผู้เรียกและนำไปใช้กับแต่ละองค์ประกอบที่เก็บข้อมูล นอกจากนี้ยังส่งผ่าน 188 ซึ่งเป็นข้อมูลที่กำหนดเองที่ส่งไปยัง 192 โดยผู้โทร ในทำนองเดียวกัน 207 ใช้กับรายการทั้งหมดในบัคเก็ตทีละรายการ โปรดทราบว่าที่เก็บข้อมูลอาจว่างเปล่า ซึ่งในกรณีนี้ค่าจะเป็น 218 .

โอเค ดังนั้นเราจึงทำซ้ำกลุ่มองค์ประกอบต่างๆ อะไรต่อไป? เราจะคืนค่าเคอร์เซอร์สำหรับการโทรครั้งถัดไปไปที่ 228 . ซึ่งทำได้โดยการเพิ่มเคอร์เซอร์ปัจจุบัน 230 แต่พลิกสถานการณ์! เคอร์เซอร์จะย้อนกลับในขั้นแรก จากนั้นเพิ่มขึ้น จากนั้นจึงย้อนกลับอีกครั้ง:

 /* Set unmasked bits so incrementing the reversed cursor
 * operates on the masked bits */
 v |= ~m0;
 /* Increment the reverse cursor */
 v = rev(v);
 v++;
 v = rev(v);

อันดับแรก 240 ตั้งค่าบิตที่ไม่ปกปิดทั้งหมดใน 253 ถึง 1. ผลที่ได้คือเมื่อถอยหลัง 267 และเพิ่มขึ้นบิตเหล่านี้จะถูกละเว้นอย่างมีประสิทธิภาพ จากนั้น 278 กลับด้าน เพิ่มขึ้น และกลับด้านอีกครั้ง ลองดูตัวอย่าง:

Table size = 16 (n = 4, m0 = 16-1 = 00001111)
v = 00001000 (Current cursor)
v |= ~m0; // v == 11111000 (~m0 = 11110000)
v = rev(v); // v == 00011111
v++; // v == 00100000
v = rev(v); // v == 00000100

หลังจากบิตมหัศจรรย์นี้ 280 ถูกส่งกลับ

เหตุใดเคอร์เซอร์จึงกลับด้านก่อนที่จะเพิ่มขึ้น ตารางอาจเพิ่มขึ้นระหว่างการวนซ้ำ สิ่งนี้รับประกันได้ว่าเคอร์เซอร์จะยังคงใช้งานได้ เมื่อตารางขยายใหญ่ขึ้น บิตเพิ่มเติมจะถูกเพิ่มลงในมาสก์ขนาด จากด้านซ้าย . ด้วยการเพิ่มจำนวนที่กลับกัน เราสามารถขยายดัชนีของตารางที่เล็กลงให้เป็นดัชนีที่ใหญ่กว่าได้

ตัวอย่างเช่น สมมติว่าโต๊ะเก่ามีขนาด 16 (มาสก์ขนาด 291 ) และเคอร์เซอร์คือ 300 . เมื่อตารางขยายเป็น 32 องค์ประกอบ ขนาดมาสก์จะเป็น 316 . องค์ประกอบทั้งหมดก่อนหน้านี้ใน 329 slot จะแมปกับ 333 หรือ 347 ในตารางใหม่ เคอร์เซอร์เหล่านี้เข้ากันได้กับตารางทั้งเล็กและใหญ่!

2. กำลังสแกนระหว่างการปรับปรุงตาราง

ส่วนสุดท้ายที่เราต้องเข้าใจคือวิธีการทำงานของการสแกนในขณะที่ตารางกำลังทำการปรับปรุงใหม่ การปรับปรุงใหม่แบบค่อยเป็นค่อยไป ถูกนำไปใช้ใน dict.c โดยมีตารางที่ใช้งานอยู่สองตารางพร้อมกัน ตารางที่สองจะถูกสร้างขึ้นเมื่อมีการปรับขนาดตารางแฮช รายการใหม่จะถูกเพิ่มลงในตารางใหม่ ในทุกขั้นตอนการปรับปรุงองค์ประกอบจากตารางเก่าจะถูกย้ายไปยังตารางใหม่ เมื่อตารางเก่าว่างเปล่า ตารางนั้นจะถูกลบออก

เมื่อทำการสแกน ตารางทั้งเก่าและใหม่จะถูกสแกนหาองค์ประกอบ โดยเริ่มจากเล็กลง ตาราง . หลังจากสแกนรายการในตารางเล็กแล้ว รายการเสริม จากตารางที่ใหญ่กว่าจะถูกสแกน ดังนั้นองค์ประกอบทั้งหมดที่ครอบคลุมโดยเคอร์เซอร์ 350 ถูกสแกน เรามาดูกันว่าสิ่งนี้มีลักษณะอย่างไร นี่คือข้อมูลโค้ดทั้งหมด เราจะแจกแจงรายละเอียดด้านล่าง:

 } else { // dictIsRehashing(d)
 t0 = &d->ht[0];
 t1 = &d->ht[1];
 /* Make sure t0 is the smaller and t1 is the bigger table */
 if (t0->size > t1->size) {
 t0 = &d->ht[1];
 t1 = &d->ht[0];
 }
 m0 = t0->sizemask;
 m1 = t1->sizemask;
 /* Emit entries at cursor */
 if (bucketfn) bucketfn(privdata, &t0->table[v & m0]);
 de = t0->table[v & m0];
 while (de) {
 next = de->next;
 fn(privdata, de);
 de = next;
 }
 /* Iterate over indices in larger table that are the expansion
 * of the index pointed to by the cursor in the smaller table */
 do {
 /* Emit entries at cursor */
 if (bucketfn) bucketfn(privdata, &t1->table[v & m1]);
 de = t1->table[v & m1];
 while (de) {
 next = de->next;
 fn(privdata, de);
 de = next;
 }
 /* Increment the reverse cursor not covered by the smaller mask.*/
 v |= ~m1;
 v = rev(v);
 v++;
 v = rev(v);
 /* Continue while bits covered by mask difference is non-zero */
 } while (v & (m0 ^ m1));
 }

ก่อนอื่น 369 และ 377 ใช้เพื่อจัดเก็บตารางเล็กและใหญ่ตามลำดับด้วย 383 และ 395 ขนาดมาส์กสำหรับแต่ละอัน จากนั้นสแกนตารางเล็กๆ เหมือนกับที่เราเห็นก่อนหน้านี้

จากนั้น เคอร์เซอร์จะถูกใช้เพื่อสร้างดัชนีลงในตารางที่ใหญ่กว่า โดยใช้มาสก์ขนาดที่ใหญ่กว่า 405 :415 . ในวงใน เคอร์เซอร์จะเพิ่มขึ้นเพื่อครอบคลุมการขยายทั้งหมดของดัชนีของตารางที่เล็กลง

ตัวอย่างเช่น หากดัชนีของที่เก็บข้อมูลในตารางที่เล็กกว่าคือ 426 และตารางที่ใหญ่กว่านั้นก็ใหญ่เป็นสองเท่า ดัชนีที่ครอบคลุมในลูปนี้จะเป็น 433 และ 441 . เงื่อนไขของ do- While ป้องกันไม่ให้เพิ่มเคอร์เซอร์เกินช่วงที่ครอบคลุมโดยที่เก็บข้อมูลของตารางขนาดเล็ก:453 . ฉันจะปล่อยให้คุณเข้าใจบิตสุดท้ายนี้ :)

แค่นั้นแหละ! เราได้ครอบคลุมฟังก์ชันการสแกนตารางแฮชทั้งหมดแล้ว สิ่งเดียวที่ขาดหายไปคือการใช้งาน 469 ซึ่งเป็นฟังก์ชันทั่วไปในการกลับบิตเป็นตัวเลข การใช้งานที่ใช้ใน dict.c มีความน่าสนใจเป็นพิเศษเนื่องจากมีเวลารัน O(log n) ฉันอาจจะกล่าวถึงในโพสต์ในอนาคต

ขอบคุณสำหรับการอ่าน! ขอขอบคุณ Dvir Volk สำหรับแรงบันดาลใจและการสนับสนุนของเขา! ขอขอบคุณ Jason Li สำหรับความคิดเห็นของเขาที่ช่วยฉันแก้ไขข้อผิดพลาดในโพสต์

เรียนรู้การเขียนโค้ดฟรี หลักสูตรโอเพ่นซอร์สของ freeCodeCamp ช่วยให้ผู้คนมากกว่า 40,000 คนได้งานในตำแหน่งนักพัฒนา เริ่มต้น