تألق وفقر قاعدة بيانات القيمة الأساسية LMDB في تطبيقات iOS

تألق وفقر قاعدة بيانات القيمة الأساسية LMDB في تطبيقات iOS

في خريف عام 2019 ، تم عقد حدث طال انتظاره في فريق Mail.ru Cloud iOS. أصبحت قاعدة البيانات الرئيسية للتخزين المستمر لحالة التطبيق غريبة تمامًا لعالم الأجهزة المحمولة قاعدة بيانات Lightning Memory-Mapped (LMDB). تحت الخفض ، انتباهك مدعو إلى المراجعة التفصيلية في أربعة أجزاء. أولاً ، دعنا نتحدث عن أسباب هذا الاختيار غير التافه والصعب. ثم دعنا ننتقل إلى التفكير في ثلاثة حيتان في قلب بنية LMDB: الملفات المعينة للذاكرة ، وشجرة B + ، وطريقة النسخ عند الكتابة لتنفيذ المعاملات والتنوع المتعدد. أخيرًا ، للحلوى - الجزء العملي. في ذلك ، سننظر في كيفية تصميم وتنفيذ مخطط أساسي مع عدة جداول ، بما في ذلك فهرس واحد ، أعلى واجهة برمجة التطبيقات ذات القيمة الرئيسية المنخفضة المستوى.

محتوى

  1. دافع التنفيذ
  2. تحديد موقع LMDB
  3. ثلاثة حيتان LMDB
    3.1 الحوت # 1. ملفات الذاكرة المعينة
    3.2 الحوت # 2. ب + شجرة
    3.3 الحوت # 3. نسخ عند الكتابة
  4. تصميم مخطط بيانات أعلى واجهة برمجة التطبيقات ذات القيمة الرئيسية
    4.1 التجريدات الأساسية
    4.2 نمذجة الجدول
    4.3 نمذجة العلاقات بين الجداول

1. الدافع للتنفيذ

مرة واحدة في العام ، في عام 2015 ، اهتممنا بأخذ مقياس ، كم مرة تتأخر واجهة تطبيقنا. نحن لم نفعل هذا فقط. لدينا المزيد والمزيد من الشكاوى حول حقيقة أن التطبيق في بعض الأحيان يتوقف عن الاستجابة لإجراءات المستخدم: لا يتم الضغط على الأزرار ، والقوائم لا يتم تمريرها ، وما إلى ذلك. حول ميكانيكا القياسات قلت على AvitoTech ، أعطي هنا فقط ترتيب الأرقام.

تألق وفقر قاعدة بيانات القيمة الأساسية LMDB في تطبيقات iOS

أصبحت نتائج القياس بمثابة دش بارد بالنسبة لنا. اتضح أن المشاكل الناجمة عن التجميد أكبر بكثير من أي مشاكل أخرى. إذا كان المؤشر الفني الرئيسي للجودة ، قبل إدراك هذه الحقيقة ، خاليًا من التعطل ، ثم بعد التركيز تحول عند التجميد.

بعد أن بنيت لوحة القيادة مع التجمد وبعد أن أمضى كمي и جودة تحليل أسبابهم ، أصبح العدو الرئيسي واضحًا - منطق الأعمال الثقيل الذي يتم تنفيذه في الموضوع الرئيسي للتطبيق. كان رد الفعل الطبيعي على هذا العار هو الرغبة الشديدة في دفعه إلى مسارات العمل. من أجل حل منهجي لهذه المشكلة ، لجأنا إلى بنية متعددة الخيوط تعتمد على جهات فاعلة خفيفة الوزن. كرست تعديلاتها لعالم iOS اثنين من المواضيع في تويتر الجماعي و مقال عن حبري. كجزء من القصة الحالية ، أود التأكيد على جوانب القرار التي أثرت في اختيار قاعدة البيانات.

يفترض النموذج الفاعل لتنظيم النظام أن تعدد مؤشرات الترابط يصبح جوهره الثاني. كائنات نموذجية فيه مثل عبور حدود الخيط. وهم يفعلون ذلك ليس أحيانًا وفي بعض الأماكن ، لكنهم يفعلون ذلك دائمًا وفي كل مكان تقريبًا.

تألق وفقر قاعدة بيانات القيمة الأساسية LMDB في تطبيقات iOS

تعد قاعدة البيانات أحد مكونات حجر الأساس في الرسم التخطيطي المقدم. مهمتها الرئيسية هي تنفيذ نمط الماكرو قاعدة البيانات المشتركة. إذا تم استخدامه في عالم المؤسسات لتنظيم مزامنة البيانات بين الخدمات ، ثم في حالة بنية الفاعل ، البيانات بين سلاسل العمليات. وبالتالي ، فقد احتجنا إلى قاعدة بيانات كهذه ، والتي لا يتسبب العمل بها في بيئة متعددة الخيوط حتى في الحد الأدنى من الصعوبات. على وجه الخصوص ، هذا يعني أن الكائنات المشتقة منه يجب أن تكون على الأقل آمنة للخيط ، ومن الناحية المثالية غير قابلة للتغيير على الإطلاق. كما تعلم ، يمكن استخدام الأخير في وقت واحد من عدة خيوط دون اللجوء إلى أي نوع من الأقفال ، مما له تأثير مفيد على الأداء.

تألق وفقر قاعدة بيانات القيمة الأساسية LMDB في تطبيقات iOSكان العامل الثاني المهم الذي أثر في اختيار قاعدة البيانات هو واجهة برمجة التطبيقات السحابية الخاصة بنا. كان مستوحى من نهج git للتزامن. كنا نستهدف مثله واجهة برمجة التطبيقات الأولى في وضع عدم الاتصال، والتي تبدو أكثر من مناسبة لعملاء السحابة. كان من المفترض أنهم سيضخون الحالة الكاملة للسحابة مرة واحدة فقط ، ومن ثم تحدث المزامنة في الغالبية العظمى من الحالات من خلال التغييرات المتدرجة. للأسف ، لا يزال هذا الاحتمال في المنطقة النظرية فقط ، ومن الناحية العملية ، لم يتعلم العملاء كيفية التعامل مع التصحيحات. هناك عدد من الأسباب الموضوعية لذلك ، والتي ، من أجل عدم تأخير المقدمة ، سوف نتركها من الأقواس. الآن أكثر إثارة للاهتمام هي النتائج الإرشادية للدرس حول ما يحدث عندما قالت API "أ" ولم يقل المستهلك "ب".

لذلك ، إذا تخيلت git ، والتي عند تنفيذ أمر سحب ، بدلاً من تطبيق التصحيحات على لقطة محلية ، تقارن حالتها الكاملة بخادم كامل ، فستكون لديك فكرة دقيقة إلى حد ما عن كيفية المزامنة يحدث في عملاء السحابة. من السهل تخمين أنه من أجل تنفيذه ، من الضروري تخصيص شجرتين من شجرتا DOM في الذاكرة بمعلومات تعريفية حول جميع الملفات الموجودة على الخادم والملفات المحلية. اتضح أنه إذا قام المستخدم بتخزين 500 ألف ملف في السحابة ، ثم لمزامنتها ، فمن الضروري إعادة إنشاء وتدمير شجرتين مع مليون عقدة. لكن كل عقدة عبارة عن تجميع يحتوي على رسم بياني للكائنات الفرعية. في ضوء ذلك ، كانت نتائج التنميط متوقعة. اتضح أنه حتى بدون مراعاة خوارزمية الدمج ، فإن إجراء إنشاء ثم تدمير عدد كبير من الكائنات الصغيرة يكلف فلساً واحداً. ويتفاقم الموقف بسبب حقيقة أن عملية المزامنة الأساسية مدرجة في عدد كبير من نصوص المستخدم. نتيجة لذلك ، نصلح المعيار الثاني المهم في اختيار قاعدة البيانات - القدرة على تنفيذ عمليات CRUD دون التخصيص الديناميكي للكائنات.

المتطلبات الأخرى أكثر تقليدية ، وقائمتهم الكاملة هي كما يلي.

  1. سلامة الخيط.
  2. المعالجة المتعددة. تمليها الرغبة في استخدام نفس مثيل قاعدة البيانات لمزامنة الحالة ليس فقط بين سلاسل الرسائل ، ولكن أيضًا بين التطبيق الرئيسي وامتدادات iOS.
  3. القدرة على تمثيل الكيانات المخزنة ككائنات غير قابلة للتغيير.
  4. عدم وجود تخصيصات ديناميكية ضمن عمليات CRUD.
  5. دعم المعاملات للخصائص الأساسية حمضالكلمات المفتاحية: الذرية ، التناسق ، العزلة والموثوقية.
  6. السرعة في التعامل مع الحالات الأكثر شيوعًا.

مع هذه المجموعة من المتطلبات ، كان SQLite ولا يزال خيارًا جيدًا. ومع ذلك ، كجزء من دراسة البدائل ، صادفت كتابًا "البدء مع LevelDB". تحت قيادتها ، تمت كتابة معيار مرجعي يقارن سرعة العمل بقواعد بيانات مختلفة في سيناريوهات السحابة الحقيقية. تجاوزت النتيجة أعنف التوقعات. في الحالات الأكثر شيوعًا - الحصول على مؤشر في قائمة مرتبة لجميع الملفات وقائمة مصنفة لجميع الملفات لدليل معين - تبين أن LMDB أسرع بعشر مرات من SQLite. أصبح الاختيار واضحًا.

تألق وفقر قاعدة بيانات القيمة الأساسية LMDB في تطبيقات iOS

2. تحديد المواقع LMDB

LMDB هي مكتبة صغيرة جدًا (فقط 10 آلاف سطر) تنفذ أدنى طبقة أساسية من قواعد البيانات - التخزين.

تألق وفقر قاعدة بيانات القيمة الأساسية LMDB في تطبيقات iOS

يوضح الرسم البياني أعلاه أن مقارنة LMDB مع SQLite ، والتي تطبق مستويات أعلى ، ليست بشكل عام أكثر صحة من SQLite مع Core Data. سيكون من الإنصاف الاستشهاد بنفس محركات التخزين مثل المنافسين المتساوين - BerkeleyDB و LevelDB و Sophia و RocksDB وما إلى ذلك. حتى أن هناك تطورات حيث يعمل LMDB كمكون محرك تخزين لـ SQLite. أول تجربة من هذا القبيل في عام 2012 قضيت المؤلف LMDB هوارد تشو. النتائج اتضح أنها كانت مثيرة للاهتمام لدرجة أن مبادرته التقطت من قبل عشاق OSS ، ووجدت استمرارها في مواجهة لوموسقل. في يناير 2020 ، مؤلف هذا المشروع هو Den Shearer قدم على LinuxConfAu.

الاستخدام الرئيسي لـ LMDB هو كمحرك لقواعد بيانات التطبيق. تدين المكتبة بمظهرها للمطورين ب OpenLDAP، الذين كانوا غير راضين بشدة عن BerkeleyDB كأساس لمشروعهم. الابتعاد عن المكتبة المتواضعة com.btree، تمكن Howard Chu من إنشاء أحد أكثر البدائل شيوعًا في عصرنا. لقد كرس تقريره الرائع لهذه القصة ، بالإضافة إلى الهيكل الداخلي لـ LMDB. "قاعدة البيانات المعينة للذاكرة Lightning". ليونيد يورييف (المعروف أيضًا باسم yleo) من شركة Positive Technologies في حديثه في Highload 2015 "محرك LMDB هو بطل خاص". في ذلك ، تحدث عن LMDB في سياق مهمة مماثلة لتنفيذ ReOpenLDAP ، وقد خضع LevelDB بالفعل للنقد المقارن. نتيجة للتنفيذ ، حصلت شركة Positive Technologies على شوكة نشطة بشكل نشط MDBX مع ميزات لذيذة للغاية وتحسينات و اصلاحات الشوائب.

غالبًا ما يستخدم LMDB كأداة تخزين أيضًا. على سبيل المثال ، متصفح Mozilla Firefox اختار لعدد من الاحتياجات ، وبدءًا من الإصدار 9 ، Xcode يفضل سكليتي لتخزين الفهارس.

اشتعل المحرك أيضًا في عالم تطوير الأجهزة المحمولة. يمكن أن تكون آثار استخدامه اكتشاف في عميل iOS لـ Telegram. ذهب LinkedIn إلى أبعد من ذلك واختار LMDB باعتباره التخزين الافتراضي لإطار تخزين البيانات المحلي الخاص به ، Rocket Data ، أخبر في مقال في عام 2016.

تقاتل LMDB بنجاح من أجل الحصول على مكان في الشمس في المكان المناسب الذي تركته BerkeleyDB بعد الانتقال تحت سيطرة Oracle. المكتبة محبوبة لسرعتها وموثوقيتها ، حتى بالمقارنة مع نوعها. كما تعلم ، لا توجد وجبات غداء مجانية ، وأود التأكيد على المقايضة التي سيتعين عليك مواجهتها عند الاختيار بين LMDB و SQLite. يوضح الرسم البياني أعلاه بوضوح كيفية تحقيق السرعة المتزايدة. أولاً ، لا ندفع مقابل طبقات تجريد إضافية فوق مساحة تخزين القرص. بالطبع ، في بنية جيدة ، ما زلت لا تستطيع الاستغناء عنها ، وستظهر حتمًا في كود التطبيق ، لكنها ستكون أرق بكثير. لن يكون لديهم ميزات غير مطلوبة بواسطة تطبيق معين ، على سبيل المثال ، دعم الاستعلامات بلغة SQL. ثانيًا ، يصبح من الممكن التنفيذ الأمثل لتعيين عمليات التطبيق لطلبات تخزين القرص. إذا كان SQLite في عملي يأتي من متوسط ​​احتياجات التطبيق المتوسط ​​، فأنت ، كمطور تطبيق ، تدرك جيدًا سيناريوهات التحميل الرئيسية. للحصول على حل أكثر إنتاجية ، سيتعين عليك دفع سعر أعلى لتطوير الحل الأولي ودعمه اللاحق.

3. ثلاثة حيتان LMDB

بعد النظر إلى LMDB من منظور عين الطائر ، حان الوقت للتعمق أكثر. سيتم تخصيص الأقسام الثلاثة التالية لتحليل الحيتان الرئيسية التي تقوم عليها بنية التخزين:

  1. الملفات المعينة للذاكرة كآلية للعمل مع القرص ومزامنة هياكل البيانات الداخلية.
  2. B + شجرة كمنظمة لهيكل البيانات المخزنة.
  3. النسخ عند الكتابة كنهج لتوفير خصائص معاملات ACID والتوليد المتعدد.

3.1. الحوت # 1. ملفات الذاكرة المعينة

تعد الملفات المعينة بالذاكرة عنصرًا معماريًا مهمًا حتى أنها تظهر في اسم المستودع. تقع مشكلات التخزين المؤقت ومزامنة الوصول إلى المعلومات المخزنة بالكامل تحت رحمة نظام التشغيل. لا يحتوي LMDB على أي ذاكرة تخزين مؤقت داخل نفسه. هذا قرار واع من قبل المؤلف ، لأن قراءة البيانات مباشرة من الملفات المعينة تسمح لك بقطع الكثير من الزوايا في تنفيذ المحرك. فيما يلي قائمة بعيدة عن القائمة الكاملة لبعض منهم.

  1. يصبح الحفاظ على تناسق البيانات في التخزين عند العمل معها من عدة عمليات مسؤولية نظام التشغيل. في القسم التالي ، تمت مناقشة هذا الميكانيكي بالتفصيل ومع الصور.
  2. يؤدي غياب ذاكرات التخزين المؤقت إلى التخلص تمامًا من LMDB من الحمل المرتبط بالتخصيصات الديناميكية. قراءة البيانات في الممارسة العملية هي ضبط المؤشر على العنوان الصحيح في الذاكرة الظاهرية ولا شيء أكثر من ذلك. يبدو مثل الخيال ، ولكن في مصدر المستودع ، تتركز جميع مكالمات calloc في وظيفة تكوين المستودع.
  3. يعني عدم وجود ذاكرات التخزين المؤقت أيضًا عدم وجود أقفال مرتبطة بالمزامنة للوصول إليها. القراء ، الذين يمكن أن يوجد عدد عشوائي منهم في نفس الوقت ، لا يواجهون كائن مزامنة واحد في طريقهم إلى البيانات. نتيجة لذلك ، تتمتع سرعة القراءة بقابلية توسع خطية مثالية من حيث عدد وحدات المعالجة المركزية. في LMDB ، تتم مزامنة عمليات التعديل فقط. يمكن أن يكون هناك كاتب واحد فقط في كل مرة.
  4. يحفظ الحد الأدنى من منطق التخزين المؤقت والمزامنة الرمز من نوع معقد للغاية من الأخطاء المرتبطة بالعمل في بيئة متعددة الخيوط. كانت هناك دراستان مثيرتان للاهتمام لقاعدة البيانات في مؤتمر Usenix OSDI 2014: "لم يتم إنشاء جميع أنظمة الملفات على قدم المساواة: حول تعقيد صياغة تطبيقات متوافقة مع الأعطال" и تعذيب قواعد البيانات من أجل المتعة والربح. يمكنك الحصول منها على معلومات حول الموثوقية غير المسبوقة لـ LMDB ، والتنفيذ الخالي من العيوب تقريبًا لخصائص ACID للمعاملات ، والتي تتفوق عليها في نفس SQLite.
  5. يسمح الحد الأدنى من LMDB بوضع تمثيل الآلة للرمز الخاص به بالكامل في ذاكرة التخزين المؤقت L1 للمعالج بخصائص السرعة الناتجة.

لسوء الحظ ، في iOS ، الملفات المعينة للذاكرة ليست وردية كما نرغب. للحديث عن العيوب المرتبطة بها بشكل أكثر وعياً ، من الضروري تذكر المبادئ العامة لتنفيذ هذه الآلية في أنظمة التشغيل.

معلومات عامة حول الملفات المعينة للذاكرة

تألق وفقر قاعدة بيانات القيمة الأساسية LMDB في تطبيقات iOSمع كل تطبيق قابل للتنفيذ ، يقوم نظام التشغيل بربط كيان يسمى عملية. يتم تخصيص مجموعة متجاورة من العناوين لكل عملية حيث تضع كل ما تحتاجه للعمل. تحتوي أقل العناوين على أقسام بها تعليمات برمجية وبيانات وموارد مشفرة. يأتي بعد ذلك الكتلة المتزايدة من مساحة العنوان الديناميكية ، المعروفة لنا باسم الكومة. يحتوي على عناوين الكيانات التي تظهر أثناء تشغيل البرنامج. في الجزء العلوي توجد مساحة الذاكرة المستخدمة بواسطة حزمة التطبيقات. إما أن ينمو أو يتقلص ، بمعنى آخر ، فإن حجمه له طبيعة ديناميكية أيضًا. حتى لا يتداخل المكدس والكومة ويتداخلان مع بعضهما البعض ، يتم فصلهما في نهايات مختلفة من مساحة العنوان.هناك فجوة بين القسمين الديناميكيين في الأعلى والأسفل. يتم استخدام العناوين الموجودة في هذا القسم الأوسط بواسطة نظام التشغيل للاقتران بعملية من كيانات مختلفة. على وجه الخصوص ، يمكنه تعيين مجموعة مستمرة معينة من العناوين إلى ملف على القرص. يسمى هذا الملف ملف الذاكرة المعين.

مساحة العنوان المخصصة لعملية ضخمة. من الناحية النظرية ، يقتصر عدد العناوين فقط على حجم المؤشر ، والذي يتم تحديده بواسطة شهادة النظام. إذا تم تخصيص ذاكرة فعلية لها 1 في 1 ، فإن العملية الأولى ستشغل ذاكرة الوصول العشوائي بأكملها ، ولن يكون هناك أي شك في أي تعدد المهام.

ومع ذلك ، نعلم من التجربة أن أنظمة التشغيل الحديثة يمكنها تشغيل العديد من العمليات التي تريدها في نفس الوقت. هذا ممكن بسبب حقيقة أنهم يخصصون الكثير من الذاكرة للعمليات على الورق فقط ، لكن في الواقع يتم تحميلهم في الذاكرة المادية الرئيسية فقط الجزء المطلوب هنا والآن. لذلك ، تسمى الذاكرة المرتبطة بالعملية الافتراضية.

تألق وفقر قاعدة بيانات القيمة الأساسية LMDB في تطبيقات iOS

ينظم نظام التشغيل الذاكرة الظاهرية والمادية في صفحات ذات حجم معين. بمجرد طلب صفحة معينة من الذاكرة الافتراضية ، يقوم نظام التشغيل بتحميلها في الذاكرة الفعلية ويضع المراسلات بينهما في جدول خاص. إذا لم تكن هناك فتحات مجانية ، فسيتم نسخ إحدى الصفحات التي تم تحميلها مسبقًا إلى القرص ، وتحل الصفحة المطلوبة مكانها. هذا الإجراء الذي سنعود إليه بعد قليل يسمى المبادلة. يوضح الشكل أدناه العملية الموصوفة. عليها ، تم تحميل الصفحة أ بالعنوان 0 ووضعها في صفحة الذاكرة الرئيسية بالعنوان 4. وقد انعكست هذه الحقيقة في جدول المراسلات في الخلية رقم 0.

تألق وفقر قاعدة بيانات القيمة الأساسية LMDB في تطبيقات iOS

مع الملفات المعينة للذاكرة ، فإن القصة هي نفسها تمامًا. منطقيا ، من المفترض أن يتم وضعها بشكل مستمر وكامل في مساحة العنوان الافتراضية. ومع ذلك ، فإنهم يدخلون إلى الذاكرة الفعلية صفحة بصفحة وعند الطلب فقط. تتم مزامنة تعديل هذه الصفحات مع الملف الموجود على القرص. وبالتالي ، يمكنك تنفيذ إدخال / إخراج الملف ، ببساطة العمل باستخدام وحدات البايت في الذاكرة - سيتم نقل جميع التغييرات تلقائيًا بواسطة نواة نظام التشغيل إلى الملف الأصلي.
â € <
توضح الصورة أدناه كيف يقوم LMDB بمزامنة حالته عند العمل مع قاعدة البيانات من عمليات مختلفة. من خلال تعيين الذاكرة الافتراضية لعمليات مختلفة على نفس الملف ، فإننا في الواقع نلزم نظام التشغيل بمزامنة كتل معينة من مساحات عناوينها بشكل عابر مع بعضها البعض ، وهذا هو المكان الذي يبدو فيه LMDB.
â € <

تألق وفقر قاعدة بيانات القيمة الأساسية LMDB في تطبيقات iOS

فارق بسيط هو أن LMDB يعدل ملف البيانات افتراضيًا من خلال آلية استدعاء نظام الكتابة ، ويتم عرض الملف نفسه في وضع القراءة فقط. هذا النهج له تأثيران مهمان.

النتيجة الأولى مشتركة لجميع أنظمة التشغيل. يتمثل جوهرها في إضافة حماية ضد التلف غير المقصود لقاعدة البيانات بواسطة رمز غير صحيح. كما تعلم ، فإن التعليمات القابلة للتنفيذ لعملية ما مجانية للوصول إلى البيانات من أي مكان في مساحة العنوان الخاصة بها. في الوقت نفسه ، كما تذكرنا للتو ، فإن عرض ملف في وضع القراءة والكتابة يعني أن أي تعليمات يمكن أيضًا تعديلها بالإضافة إلى ذلك. إذا فعلت ذلك عن طريق الخطأ ، محاولًا ، على سبيل المثال ، الكتابة بالفعل فوق عنصر مصفوفة في فهرس غير موجود ، وبهذه الطريقة يمكنها عن طريق الخطأ تغيير الملف المعين لهذا العنوان ، مما سيؤدي إلى تلف قاعدة البيانات. إذا تم عرض الملف في وضع القراءة فقط ، فإن محاولة تغيير مساحة العنوان المقابلة لها ستؤدي إلى تعطل البرنامج مع الإشارة SIGSEGV، وسيبقى الملف كما هو.

النتيجة الثانية خاصة بنظام iOS بالفعل. لم يذكره المؤلف ولا أي مصادر أخرى صراحة ، ولكن بدونه ، لن يكون LMDB مناسبًا للتشغيل على نظام تشغيل الهاتف المحمول هذا. القسم التالي مخصص للنظر فيه.

تفاصيل الملفات المعينة للذاكرة في iOS

في عام 2018 ، كان هناك تقرير رائع في WWDC الغوص العميق في ذاكرة iOS. يخبرنا أنه في نظام iOS ، تنتمي جميع الصفحات الموجودة في الذاكرة الفعلية إلى واحد من ثلاثة أنواع: قذرة ومضغوطة ونظيفة.

تألق وفقر قاعدة بيانات القيمة الأساسية LMDB في تطبيقات iOS

الذاكرة النظيفة عبارة عن مجموعة من الصفحات التي يمكن تبديلها بأمان من الذاكرة الفعلية. يمكن إعادة تحميل البيانات التي تحتويها من مصادرها الأصلية حسب الحاجة. تقع الملفات المعيّنة للذاكرة للقراءة فقط ضمن هذه الفئة. لا يخشى نظام iOS تفريغ الصفحات التي تم تعيينها إلى ملف من الذاكرة في أي وقت ، حيث إنها مضمونة لتتم مزامنتها مع الملف الموجود على القرص.
â € <
تدخل جميع الصفحات المعدلة في الذاكرة المتسخة ، بغض النظر عن مكان وجودها في الأصل. على وجه الخصوص ، سيتم أيضًا تصنيف الملفات المعينة للذاكرة التي تم تعديلها عن طريق الكتابة إلى الذاكرة الظاهرية المرتبطة بها بهذه الطريقة. فتح LMDB مع العلم MDB_WRITEMAP، بعد إجراء التغييرات عليه ، يمكنك أن ترى بنفسك.

بمجرد أن يبدأ التطبيق في شغل الكثير من الذاكرة الفعلية ، يضغط iOS صفحاته المتسخة. مجموعة الذاكرة التي تشغلها الصفحات المتسخة والمضغوطة هي ما يسمى بصمة ذاكرة التطبيق. عندما تصل إلى قيمة حدية معينة ، يأتي برنامج OOM killer system الخفي بعد العملية وينهيها بالقوة. هذه هي خصوصية iOS مقارنة بأنظمة تشغيل سطح المكتب. في المقابل ، لا يتم توفير تقليل مساحة الذاكرة عن طريق تبديل الصفحات من الذاكرة الفعلية إلى القرص في نظام التشغيل iOS. يمكن للمرء أن يخمن فقط الأسباب. ربما يكون الإجراء الخاص بنقل الصفحات بشكل مكثف إلى القرص والعودة مستهلكًا للطاقة بشكل كبير بالنسبة للأجهزة المحمولة ، أو يحفظ iOS مورد إعادة كتابة الخلايا على أقراص SSD ، أو ربما لم يكن المصممون راضين عن الأداء العام للنظام ، حيث يكون كل شيء مبادلة باستمرار. مهما كان الأمر ، تظل الحقيقة.

الخبر السار ، الذي سبق ذكره ، هو أن LMDB لا يستخدم آلية mmap لتحديث الملفات افتراضيًا. ويترتب على ذلك أن البيانات المقدمة مصنفة كذاكرة نظيفة بواسطة iOS ولا تساهم في بصمة الذاكرة. يمكن التحقق من ذلك باستخدام أداة Xcode المسماة VM Tracker. توضح لقطة الشاشة أدناه حالة الذاكرة الافتراضية لتطبيق iOS Cloud أثناء التشغيل. في البداية ، تمت تهيئة 2 مثيلات LMDB فيه. تم السماح للأول بتعيين ملفه إلى 1 جيجا بايت من الذاكرة الافتراضية ، والثاني - 512 ميجا بايت. على الرغم من حقيقة أن كلا المخزنين يشغلان قدرًا معينًا من الذاكرة المقيمة ، إلا أن أيا منهما لا يساهم في الحجم المتسخ.

تألق وفقر قاعدة بيانات القيمة الأساسية LMDB في تطبيقات iOS

حان الوقت الآن للأخبار السيئة. بفضل آلية المبادلة في أنظمة تشغيل سطح المكتب 64 بت ، يمكن أن تشغل كل عملية مساحة العنوان الافتراضية بقدر ما تسمح المساحة الخالية على القرص الثابت بالتبديل المحتمل. يؤدي استبدال الضغط في نظام iOS إلى تقليل الحد الأقصى النظري بشكل كبير. الآن يجب أن تتناسب جميع العمليات الحية مع الذاكرة الرئيسية (قراءة RAM) ، وجميع العمليات غير الملائمة تخضع للإنهاء القسري. وهو مذكور كما في أعلاه نقل، وفي الوثائق الرسمية. نتيجة لذلك ، يحد نظام iOS بشدة من حجم الذاكرة المتاحة للتخصيص عبر mmap. هنا هنا يمكنك إلقاء نظرة على الحدود التجريبية لمقدار الذاكرة التي يمكن تخصيصها على أجهزة مختلفة باستخدام استدعاء النظام هذا. في أحدث طرازات الهواتف الذكية ، أصبح نظام iOS سخيًا بمقدار 2 غيغابايت ، وفي الإصدارات العليا من iPad - بنسبة 4. من الناحية العملية ، بالطبع ، عليك التركيز على أحدث طرازات الأجهزة المدعومة ، حيث كل شيء محزن للغاية. والأسوأ من ذلك ، بالنظر إلى حالة ذاكرة التطبيق في VM Tracker ، ستجد أن LMDB بعيدًا عن الوحيد الذي يدعي الذاكرة المعينة للذاكرة. يتم التخلص من الأجزاء الجيدة من خلال مخصصات النظام وملفات الموارد وأطر الصور وغيرها من الحيوانات المفترسة الأصغر.

نتيجة للتجارب في السحابة ، توصلنا إلى القيم الوسطية التالية للذاكرة المخصصة بواسطة LMDB: 384 ميجابايت للأجهزة 32 بت و 768 للأجهزة 64 بت. بعد استخدام هذا المجلد ، تبدأ أي عمليات تعديل في الاكتمال بالرمز MDB_MAP_FULL. نلاحظ مثل هذه الأخطاء في مراقبتنا ، لكنها صغيرة بما يكفي لإهمالها في هذه المرحلة.

يمكن أن يكون السبب غير الواضح للاستهلاك المفرط للذاكرة عن طريق التخزين هو المعاملات طويلة الأمد. لفهم كيفية ارتباط هاتين الظاهرتين ، سيساعدنا ذلك على التفكير في الحيتان المتبقيتين LMDB.

3.2 الحوت # 2. ب + شجرة

لمحاكاة الجداول الموجودة أعلى مخزن القيمة الرئيسية ، يجب أن تكون العمليات التالية موجودة في واجهة برمجة التطبيقات الخاصة به:

  1. ادراج عنصر جديد.
  2. ابحث عن عنصر بمفتاح معين.
  3. حذف عنصر.
  4. كرر على فترات المفاتيح بترتيب الفرز.

تألق وفقر قاعدة بيانات القيمة الأساسية LMDB في تطبيقات iOSأبسط بنية بيانات يمكنها بسهولة تنفيذ جميع العمليات الأربع هي شجرة بحث ثنائية. كل عقد من عقده عبارة عن مفتاح يقسم المجموعة الفرعية الكاملة للمفاتيح الفرعية إلى شجرتين فرعيتين. على اليسار توجد تلك التي تكون أصغر من الأب ، وعلى اليمين - تلك التي تكون أكبر. يتم الحصول على مجموعة مرتبة من المفاتيح من خلال إحدى عمليات اجتياز الشجرة الكلاسيكية.

للأشجار الثنائية عيبان أساسيان يمنعانها من أن تكون فعالة كهيكل بيانات قرص. أولاً ، درجة توازنهم لا يمكن التنبؤ بها. هناك خطر كبير في الحصول على الأشجار التي يمكن أن يختلف فيها ارتفاع الفروع المختلفة بشكل كبير ، مما يؤدي إلى تفاقم التعقيد الحسابي للبحث مقارنة بما هو متوقع. ثانيًا ، تحرم وفرة الروابط المتقاطعة بين العقد الأشجار الثنائية من المنطقة في الذاكرة ، ويمكن أن توجد العقد القريبة (من حيث الروابط بينها) في صفحات مختلفة تمامًا في الذاكرة الافتراضية. نتيجة لذلك ، قد يتطلب حتى اجتياز بسيط لعدة عقد مجاورة في شجرة زيارة عدد مماثل من الصفحات. هذه مشكلة حتى عندما نتحدث عن فعالية الأشجار الثنائية كهيكل بيانات في الذاكرة ، لأن الصفحات المتناوبة باستمرار في ذاكرة التخزين المؤقت للمعالج ليست رخيصة. عندما يتعلق الأمر برفع الصفحات المتعلقة بالعقدة من القرص بشكل متكرر ، تصبح الأمور سيئة حقًا. مؤسف.

تألق وفقر قاعدة بيانات القيمة الأساسية LMDB في تطبيقات iOSB- الأشجار ، كونها تطورًا للأشجار الثنائية ، تحل المشكلات المحددة في الفقرة السابقة. أولا ، هم يتوازنون ذاتيا. ثانيًا ، تقسم كل من العقد الخاصة بهم مجموعة المفاتيح الفرعية ليس إلى 2 ، ولكن إلى مجموعات فرعية مرتبة M ، ويمكن أن يكون الرقم M كبيرًا جدًا ، بترتيب من عدة مئات أو حتى آلاف.

بذلك:

  1. تحتوي كل عقدة على عدد كبير من المفاتيح المطلوبة بالفعل والأشجار منخفضة جدًا.
  2. تكتسب الشجرة خاصية المنطقة المحلية في الذاكرة ، حيث توجد المفاتيح القريبة من حيث القيمة بشكل طبيعي بجوار بعضها البعض على عقد أو عقد مجاورة.
  3. يقلل من عدد عقد النقل عند نزول الشجرة أثناء عملية البحث.
  4. يقلل من عدد العقد المستهدفة التي تمت قراءتها لاستعلامات النطاق ، نظرًا لأن كل واحدة منها تحتوي بالفعل على عدد كبير من المفاتيح المرتبة.

تألق وفقر قاعدة بيانات القيمة الأساسية LMDB في تطبيقات iOS

يستخدم LMDB متغيرًا من B-tree يسمى شجرة B + لتخزين البيانات. يوضح الرسم البياني أعلاه الأنواع الثلاثة من العقد التي يحتوي عليها:

  1. في الأعلى يوجد الجذر. إنه لا يتجسد أكثر من مفهوم قاعدة البيانات داخل المستودع. ضمن مثيل LMDB واحد ، يمكنك إنشاء قواعد بيانات متعددة تشترك في مساحة العنوان الظاهرية المعينة. كل واحد منهم يبدأ من جذره.
  2. في أدنى مستوى هي الأوراق (ورقة). هم وفقط هم الذين يحتويون على أزواج المفتاح والقيمة المخزنة في قاعدة البيانات. بالمناسبة ، هذه هي خصوصية أشجار B +. إذا كانت شجرة B العادية تخزن أجزاء القيمة في العقد من جميع المستويات ، فإن التباين B + يكون فقط في أدنى مستوى. بعد إصلاح هذه الحقيقة ، في ما يلي سنسمي النوع الفرعي للشجرة المستخدمة في LMDB ببساطة شجرة ب.
  3. بين الجذر والأوراق ، هناك 0 أو أكثر من المستويات الفنية مع عقد التنقل (الفرع). مهمتهم هي تقسيم مجموعة المفاتيح المصنفة بين الأوراق.

ماديًا ، العقد عبارة عن كتل من الذاكرة بطول محدد مسبقًا. حجمها هو مضاعف لحجم صفحات الذاكرة في نظام التشغيل ، والذي تحدثنا عنه أعلاه. يتم عرض بنية العقدة أدناه. يحتوي العنوان على معلومات وصفية ، وأكثرها وضوحًا ، على سبيل المثال ، هو المجموع الاختباري. تأتي بعد ذلك معلومات حول عمليات الإزاحة ، والتي توجد جنبًا إلى جنب مع الخلايا التي تحتوي على بيانات. يمكن أن يكون دور البيانات إما مفاتيح ، إذا كنا نتحدث عن عقد التنقل ، أو أزواج قيمة مفتاح كاملة في حالة الأوراق.يمكنك قراءة المزيد حول بنية الصفحات في العمل "تقييم الأداء العالي لمتاجر القيمة الرئيسية".

تألق وفقر قاعدة بيانات القيمة الأساسية LMDB في تطبيقات iOS

بعد التعامل مع المحتوى الداخلي لعقد الصفحة ، سنقوم بتمثيل شجرة LMDB B بطريقة مبسطة بالشكل التالي.

تألق وفقر قاعدة بيانات القيمة الأساسية LMDB في تطبيقات iOS

يتم ترتيب الصفحات ذات العقد بالتسلسل على القرص. توجد الصفحات ذات الرقم الأعلى في نهاية الملف. تحتوي ما يسمى بالصفحة الوصفية (صفحة التعريف) على معلومات حول التعويضات ، والتي يمكن استخدامها للعثور على جذور جميع الأشجار. عند فتح ملف ، يقوم LMDB بمسح صفحة الملف بصفحة من النهاية إلى البداية بحثًا عن صفحة تعريف صالحة والعثور على قواعد البيانات الموجودة من خلالها.

تألق وفقر قاعدة بيانات القيمة الأساسية LMDB في تطبيقات iOS

الآن ، مع وجود فكرة عن الهيكل المنطقي والمادي لتنظيم البيانات ، يمكننا المضي قدمًا في التفكير في الحوت الثالث من LMDB. بفضل مساعدتها ، تحدث جميع تعديلات التخزين بشكل معاملات وبمعزل عن بعضها البعض ، مما يمنح قاعدة البيانات ككل أيضًا خاصية multiversion.

3.3 الحوت # 3. نسخ عند الكتابة

تتضمن بعض عمليات B-tree إجراء سلسلة كاملة من التغييرات على عقدها. أحد الأمثلة هو إضافة مفتاح جديد إلى عقدة وصلت بالفعل إلى أقصى سعتها. في هذه الحالة ، من الضروري ، أولاً ، تقسيم العقدة إلى قسمين ، وثانيًا ، إضافة ارتباط إلى العقدة الفرعية الجديدة التي تم نسجها في الأصل. من المحتمل أن يكون هذا الإجراء خطيرًا للغاية. إذا حدث لسبب ما (تعطل ، انقطاع التيار الكهربائي ، وما إلى ذلك) سوى جزء من التغييرات من السلسلة ، فستظل الشجرة في حالة غير متسقة.

أحد الحلول التقليدية لإنشاء قاعدة بيانات متسامحة مع الأخطاء هو إضافة هيكل بيانات إضافي قائم على القرص ، سجل المعاملات ، المعروف أيضًا باسم سجل الكتابة المسبقة (WAL) ، بجوار شجرة B. إنه ملف ، في نهايته ، بدقة قبل تعديل B-tree نفسها ، تتم كتابة العملية المقصودة. وبالتالي ، إذا تم اكتشاف تلف في البيانات أثناء التشخيص الذاتي ، فإن قاعدة البيانات تستشير السجل لتنظيف نفسه.

اختارت LMDB طريقة مختلفة كآلية تحمل الخطأ ، والتي تسمى النسخ عند الكتابة. جوهرها هو أنه بدلاً من تحديث البيانات الموجودة على صفحة موجودة ، تقوم أولاً بنسخها بالكامل وإجراء جميع التعديلات الموجودة بالفعل في النسخة.

تألق وفقر قاعدة بيانات القيمة الأساسية LMDB في تطبيقات iOS

علاوة على ذلك ، لكي تكون البيانات المحدثة متاحة ، من الضروري تغيير الارتباط إلى العقدة التي أصبحت محدثة في العقدة الأصلية فيما يتعلق بها. نظرًا لأنه يحتاج أيضًا إلى تعديل لهذا الغرض ، يتم أيضًا نسخه مسبقًا. تستمر العملية بشكل متكرر على طول الطريق إلى الجذر. البيانات الموجودة في صفحة التعريف هي آخر البيانات التي يتم تغييرها.

تألق وفقر قاعدة بيانات القيمة الأساسية LMDB في تطبيقات iOS

إذا تعطلت العملية فجأة أثناء إجراء التحديث ، فلن يتم إنشاء صفحة تعريف جديدة ، أو لن تتم كتابتها على القرص حتى النهاية ، وسيكون مجموعها الاختباري غير صحيح. في أي من هاتين الحالتين ، لن يمكن الوصول إلى الصفحات الجديدة ولن تتأثر الصفحات القديمة. هذا يلغي الحاجة إلى LMDB لكتابة السجل المسبق للحفاظ على تناسق البيانات. في الواقع ، تأخذ بنية تخزين البيانات على القرص ، الموصوفة أعلاه ، وظيفتها في وقت واحد. يعد عدم وجود سجل معاملات صريح إحدى ميزات LMDB ، التي توفر سرعة قراءة عالية للبيانات.

تألق وفقر قاعدة بيانات القيمة الأساسية LMDB في تطبيقات iOS

يوفر البناء الناتج ، المسمى شجرة B الملحقة فقط ، بشكل طبيعي عزل المعاملات والتعدد المتعدد. في LMDB ، تحتوي كل معاملة مفتوحة على جذر شجرة محدث مرتبط بها. طالما لم تكتمل المعاملة ، فلن يتم تغيير صفحات الشجرة المرتبطة بها أو إعادة استخدامها لإصدارات جديدة من البيانات. وبالتالي ، يمكنك العمل طالما أردت تمامًا مع مجموعة البيانات ذات الصلة في وقت فتح المعاملة ، حتى إذا استمر تحديث التخزين بنشاط في هذا الوقت. هذا هو جوهر التعدد ، مما يجعل LMDB مصدر بيانات مثالي لأحبائنا UICollectionView. بعد فتح معاملة ، لا تحتاج إلى زيادة مساحة الذاكرة للتطبيق ، حيث تضخ البيانات الحالية على عجل في بنية ما في الذاكرة ، خوفًا من عدم ترك أي شيء. تميز هذه الميزة LMDB عن نفس SQLite ، والتي لا يمكنها التباهي بمثل هذه العزلة الكاملة. بعد فتح صفقتين في الأخير وحذف سجل معين داخل إحداها ، لم يعد من الممكن الحصول على نفس السجل في الثانية المتبقية.

الوجه الآخر للعملة هو الاستهلاك الأعلى المحتمل للذاكرة الافتراضية. تعرض الشريحة الشكل الذي ستبدو عليه بنية قاعدة البيانات إذا تم تعديلها في نفس الوقت بثلاث معاملات قراءة مفتوحة تبحث في إصدارات مختلفة من قاعدة البيانات. نظرًا لأن LMDB لا يمكنه إعادة استخدام العقد التي يمكن الوصول إليها من الجذور المرتبطة بالمعاملات الفعلية ، فلا خيار للتخزين سوى تخصيص جذر رابع آخر في الذاكرة واستنساخ الصفحات المعدلة تحته مرة أخرى.

تألق وفقر قاعدة بيانات القيمة الأساسية LMDB في تطبيقات iOS

هنا لن يكون من الضروري تذكر القسم الخاص بالملفات المعينة للذاكرة. يبدو أن الاستهلاك الإضافي للذاكرة الافتراضية لا ينبغي أن يزعجنا كثيرًا ، لأنه لا يساهم في بصمة ذاكرة التطبيق. ومع ذلك ، في الوقت نفسه ، لوحظ أن نظام iOS بخيل جدًا في تخصيصه ، ولا يمكننا توفير منطقة LMDB بسعة 1 تيرابايت على خادم أو سطح مكتب من كتف السيد ولا نفكر في هذه الميزة على الإطلاق. عندما يكون ذلك ممكنًا ، يجب أن تحاول الحفاظ على عمر المعاملات قصيرًا قدر الإمكان.

4. تصميم مخطط بيانات أعلى واجهة برمجة التطبيقات ذات القيمة الرئيسية

لنبدأ في تحليل API من خلال النظر في التجريدات الأساسية التي يوفرها LMDB: البيئة وقواعد البيانات والمفاتيح والقيم والمعاملات والمؤشرات.

ملاحظة حول قوائم التعليمات البرمجية

تعرض جميع الوظائف في LMDB public API نتيجة عملها في شكل رمز خطأ ، ولكن في جميع القوائم اللاحقة ، تم حذف الاختيار من أجل الإيجاز. في الممارسة العملية ، استخدمنا الكود الخاص بنا للتفاعل مع المستودع. شوكة أغلفة C ++ com.lmdbxx، حيث تتحقق الأخطاء كاستثناءات C ++.

باعتبارها أسرع طريقة لتوصيل LMDB بمشروع iOS أو macOS ، أقدم تطبيق CocoaPod الخاص بي POSLMDB.

4.1 التجريدات الأساسية

بيئة

هيكل MDB_env هو مستودع الحالة الداخلية لـ LMDB. عائلة وظائف مسبوقة mdb_env يسمح لك بتكوين بعض خصائصه. في أبسط الحالات ، تبدو تهيئة المحرك هكذا.

mdb_env_create(env);​
mdb_env_set_map_size(*env, 1024 * 1024 * 512)​
mdb_env_open(*env, path.UTF8String, MDB_NOTLS, 0664);

في تطبيق Mail.ru Cloud ، قمنا بتغيير القيم الافتراضية لمعلمتين فقط.

الأول هو حجم مساحة العنوان الظاهرية التي تم تعيين ملف التخزين إليها. لسوء الحظ ، حتى على نفس الجهاز ، يمكن أن تختلف القيمة المحددة بشكل كبير من تشغيل إلى آخر. لمراعاة ميزة iOS هذه ، نختار الحد الأقصى لمقدار التخزين ديناميكيًا. بدءًا من قيمة معينة ، يتم تقسيمها إلى النصف على التوالي حتى الدالة mdb_env_open لن يعود بنتيجة أخرى غير ENOMEM. من الناحية النظرية ، هناك طريقة معاكسة - أولاً قم بتخصيص حد أدنى من الذاكرة للمحرك ، ثم عند تلقي الأخطاء MDB_MAP_FULL، زيادته. ومع ذلك ، فهي أكثر شائكة. والسبب هو أن إجراء إعادة تعيين الذاكرة باستخدام الوظيفة mdb_env_set_map_size يبطل جميع الكيانات (المؤشرات والمعاملات والمفاتيح والقيم) المستلمة من المحرك مسبقًا. سيؤدي حساب مثل هذا التحول في الأحداث في الكود إلى تعقيده الكبير. ومع ذلك ، إذا كانت الذاكرة الافتراضية عزيزة جدًا عليك ، فقد يكون هذا سببًا للنظر إلى المفترق الذي مضى بعيدًا. MDBX، حيث يوجد من بين الميزات المعلنة "تعديل تلقائي لحجم قاعدة البيانات أثناء التنقل".

المعلمة الثانية ، القيمة الافتراضية لها لا تناسبنا ، تنظم آليات ضمان سلامة الخيط. لسوء الحظ ، على الأقل في iOS 10 ، هناك مشاكل في دعم التخزين المحلي لمؤشر الترابط. لهذا السبب ، في المثال أعلاه ، يتم فتح المستودع بالعلامة MDB_NOTLS. بالإضافة إلى ذلك ، فإنه مطلوب أيضًا شوكة غلاف C ++ com.lmdbxxلقطع المتغيرات مع وفي هذه السمة.

قواعد البيانات

قاعدة البيانات هي مثيل منفصل لشجرة B التي تحدثنا عنها أعلاه. يتم فتحه داخل صفقة ، والتي قد تبدو غريبة بعض الشيء في البداية.

MDB_txn *txn;​
MDB_dbi dbi;​
mdb_txn_begin(env, NULL, MDB_RDONLY, &txn);​
mdb_dbi_open(txn, NULL, MDB_CREATE, &dbi);​
mdb_txn_abort(txn);

في الواقع ، تعتبر المعاملة في LMDB كيان تخزين ، وليست قاعدة بيانات محددة. يتيح لك هذا المفهوم إجراء عمليات ذرية على كيانات موجودة في قواعد بيانات مختلفة. من الناحية النظرية ، يفتح هذا إمكانية نمذجة الجداول في شكل قواعد بيانات مختلفة ، لكن في وقت ما ذهبت في الاتجاه الآخر ، الموصوف بالتفصيل أدناه.

المفاتيح والقيم

هيكل MDB_val نماذج لمفهوم كل من المفتاح والقيمة. المستودع ليس لديه فكرة عن دلالاتها. بالنسبة لها ، الشيء المختلف هو مجرد مجموعة من البايتات ذات الحجم المحدد. الحجم الأقصى للمفتاح هو 512 بايت.

typedef struct MDB_val {​
    size_t mv_size;​
    void *mv_data;​
} MDB_val;​​

يستخدم المتجر أداة مقارنة لفرز المفاتيح بترتيب تصاعدي. إذا لم تقم باستبداله بآخر خاص بك ، فسيتم استخدام الافتراضي ، والذي يقوم بفرزها بايتًا بايتًا بترتيب معجمي.

المعاملات

جهاز المعاملة موصوف بالتفصيل في الفصل السابقلذلك سأكرر هنا خصائصهم الرئيسية في سطر قصير:

  1. دعم لجميع الخصائص الأساسية حمضالكلمات المفتاحية: الذرية ، التناسق ، العزلة والموثوقية. لا يسعني إلا أن أشير إلى أنه من حيث المتانة على macOS و iOS ، هناك خطأ ثابت في MDBX. يمكنك قراءة المزيد في README.
  2. يتم وصف نهج تعدد مؤشرات الترابط بواسطة مخطط "كاتب واحد / عدة قراء". يحظر الكتاب بعضهم البعض ، لكنهم لا يمنعون القراء. القراء لا يمنعون الكتاب أو بعضهم البعض.
  3. دعم المعاملات المتداخلة.
  4. دعم Multiversion.

تعد عمليات التحويل المتعددة في LMDB جيدة جدًا لدرجة أنني أرغب في إثباتها عمليًا. يوضح الكود أدناه أن كل معاملة تعمل بالضبط مع إصدار قاعدة البيانات الذي كان ذا صلة وقت فتحها ، حيث يتم عزلها تمامًا عن جميع التغييرات اللاحقة. لا يهم تهيئة المستودع وإضافة سجل اختبار إليه ، لذلك تُترك هذه الطقوس تحت المفسد.

إضافة إدخال اختبار

MDB_env *env;
MDB_dbi dbi;
MDB_txn *txn;

mdb_env_create(&env);
mdb_env_open(env, "./testdb", MDB_NOTLS, 0664);

mdb_txn_begin(env, NULL, 0, &txn);
mdb_dbi_open(txn, NULL, 0, &dbi);
mdb_txn_abort(txn);

char k = 'k';
MDB_val key;
key.mv_size = sizeof(k);
key.mv_data = (void *)&k;

int v = 997;
MDB_val value;
value.mv_size = sizeof(v);
value.mv_data = (void *)&v;

mdb_txn_begin(env, NULL, 0, &txn);
mdb_put(txn, dbi, &key, &value, MDB_NOOVERWRITE);
mdb_txn_commit(txn);

MDB_txn *txn1, *txn2, *txn3;
MDB_val val;

// Открываем 2 транзакции, каждая из которых смотрит
// на версию базы данных с одной записью.
mdb_txn_begin(env, NULL, 0, &txn1); // read-write
mdb_txn_begin(env, NULL, MDB_RDONLY, &txn2); // read-only

// В рамках первой транзакции удаляем из базы данных существующую в ней запись.
mdb_del(txn1, dbi, &key, NULL);
// Фиксируем удаление.
mdb_txn_commit(txn1);

// Открываем третью транзакцию, которая смотрит на
// актуальную версию базы данных, где записи уже нет.
mdb_txn_begin(env, NULL, MDB_RDONLY, &txn3);
// Убеждаемся, что запись по искомому ключу уже не существует.
assert(mdb_get(txn3, dbi, &key, &val) == MDB_NOTFOUND);
// Завершаем транзакцию.
mdb_txn_abort(txn3);

// Убеждаемся, что в рамках второй транзакции, открытой на момент
// существования записи в базе данных, её всё ещё можно найти по ключу.
assert(mdb_get(txn2, dbi, &key, &val) == MDB_SUCCESS);
// Проверяем, что по ключу получен не абы какой мусор, а валидные данные.
assert(*(int *)val.mv_data == 997);
// Завершаем транзакцию, работающей хоть и с устаревшей, но консистентной базой данных.
mdb_txn_abort(txn2);

اختياريًا ، أوصي بتجربة نفس الحيلة مع SQLite ومعرفة ما سيحدث.

يوفر التحويل المتعدد فوائد رائعة جدًا لحياة مطور iOS. باستخدام هذه الخاصية ، يمكنك بسهولة وبطبيعة الحال ضبط معدل تحديث مصدر البيانات لنماذج الشاشة بناءً على اعتبارات تجربة المستخدم. على سبيل المثال ، لنأخذ ميزة تطبيق Mail.ru Cloud مثل تحميل المحتوى تلقائيًا من معرض وسائط النظام. من خلال الاتصال الجيد ، يمكن للعميل إضافة عدة صور في الثانية إلى الخادم. إذا قمت بالتحديث بعد كل تنزيل UICollectionView مع محتوى الوسائط في سحابة المستخدم ، يمكنك نسيان حوالي 60 إطارًا في الثانية والتمرير السلس أثناء هذه العملية. لمنع تحديثات الشاشة المتكررة ، تحتاج إلى الحد بطريقة ما من معدل تغيير البيانات في الأساس UICollectionViewDataSource.

إذا كانت قاعدة البيانات لا تدعم التحويل المتعدد وتسمح لك بالعمل فقط مع الحالة الحالية ، ثم لإنشاء لقطة بيانات ثابتة زمنياً ، فأنت بحاجة إلى نسخها إما إلى بعض هياكل البيانات الموجودة في الذاكرة أو إلى جدول مؤقت. أي من هذه الأساليب مكلف للغاية. في حالة التخزين في الذاكرة ، نحصل على كل من تكاليف الذاكرة الناتجة عن تخزين الكائنات المُنشأة وتكاليف الوقت المرتبطة بتحويلات ORM الزائدة عن الحاجة. أما بالنسبة للجدول المؤقت ، فهذه متعة أكثر تكلفة ، وهو أمر منطقي فقط في الحالات غير التافهة.

يحل LMDB متعدد الاستخدامات مشكلة الحفاظ على مصدر بيانات مستقر بطريقة أنيقة للغاية. يكفي فقط فتح معاملة وفويلا - حتى نكملها ، نضمن لك إصلاح مجموعة البيانات. أصبح منطق معدل التحديث الخاص به الآن بالكامل في أيدي طبقة العرض التقديمي ، دون وجود نفقات إضافية لموارد كبيرة.

المؤشرات

توفر المؤشرات آلية للتكرار المنظم على أزواج القيمة الرئيسية عن طريق اجتياز شجرة B. بدونها ، سيكون من المستحيل وضع نماذج فعالة للجداول في قاعدة البيانات ، والتي ننتقل إليها الآن.

4.2 نمذجة الجدول

تتيح لك خاصية ترتيب المفاتيح إنشاء تجريد عالي المستوى مثل جدول أعلى التجريدات الأساسية. لنفكر في هذه العملية في مثال الجدول الرئيسي لعميل السحابة ، حيث يتم تخزين المعلومات حول جميع ملفات ومجلدات المستخدم مؤقتًا.

مخطط الجدول

أحد السيناريوهات الشائعة التي يجب أن يتم فيها صقل بنية الجدول باستخدام شجرة مجلد هو تحديد جميع العناصر الموجودة داخل دليل معين. نموذج تنظيم البيانات الجيد للاستعلامات الفعالة من هذا النوع هو قائمة الجوار. لتنفيذه فوق تخزين قيمة المفتاح ، من الضروري فرز مفاتيح الملفات والمجلدات بطريقة يتم تجميعها بناءً على الانتماء إلى الدليل الأصلي. بالإضافة إلى ذلك ، لعرض محتويات الدليل بالشكل المألوف لمستخدم Windows (المجلدات أولاً ، ثم الملفات ، كلاهما مرتبة أبجديًا) ، من الضروري تضمين الحقول الإضافية المقابلة في المفتاح.

توضح الصورة أدناه كيف ، بناءً على المهمة ، قد يبدو تمثيل المفاتيح كمصفوفة من البايتات. أولاً ، يتم وضع وحدات البايت التي تحتوي على معرف الدليل الأصلي (أحمر) ، ثم بالنوع (أخضر) ، وبالفعل في الذيل بالاسم (أزرق). يجري فرزها بواسطة مقارنة LMDB الافتراضية بترتيب معجمي ، يتم ترتيبها في بالطريقة المطلوبة. يعطينا الانتقال المتسلسل للمفاتيح ذات البادئة الحمراء نفسها القيم المرتبطة بها بالترتيب الذي ينبغي عرضها به في واجهة المستخدم (يمين) ، دون الحاجة إلى أي معالجة لاحقة إضافية.

تألق وفقر قاعدة بيانات القيمة الأساسية LMDB في تطبيقات iOS

تسلسل المفاتيح والقيم

هناك العديد من الطرق لتسلسل العناصر حول العالم. نظرًا لعدم وجود متطلبات أخرى غير السرعة ، اخترنا أسرع واحد ممكن لأنفسنا - تفريغ ذاكرة مشغول بمثيل من بنية لغة C. لذلك ، يمكن نمذجة مفتاح عنصر الدليل بالهيكل التالي NodeKey.

typedef struct NodeKey {​
    EntityId parentId;​
    uint8_t type;​
    uint8_t nameBuffer[256];​
} NodeKey;

للحفظ NodeKey في حاجة التخزين في الكائن MDB_val ضع المؤشر على البيانات في عنوان بداية الهيكل ، واحسب حجمها باستخدام الوظيفة sizeof.

MDB_val serialize(NodeKey * const key) {
    return MDB_val {
        .mv_size = sizeof(NodeKey),
        .mv_data = (void *)key
    };
}

في الفصل الأول حول معايير اختيار قاعدة البيانات ، ذكرت تقليل التخصيصات الديناميكية كجزء من عمليات CRUD كعامل اختيار مهم. كود الوظيفة serialize يوضح كيف ، في حالة LMDB ، يمكن تجنبها تمامًا عند إدراج سجلات جديدة في قاعدة البيانات. يتم تحويل مجموعة البايت الواردة من الخادم أولاً إلى هياكل مكدسة ، ثم يتم تفريغها بشكل تافه في التخزين. نظرًا لعدم وجود تخصيصات ديناميكية داخل LMDB أيضًا ، يمكنك الحصول على وضع رائع وفقًا لمعايير iOS - استخدم فقط ذاكرة المكدس للعمل مع البيانات على طول الطريق من الشبكة إلى القرص!

ترتيب المفاتيح بمقارن ثنائي

يتم إعطاء علاقة ترتيب المفتاح بواسطة وظيفة خاصة تسمى المقارنة. نظرًا لأن المحرك لا يعرف شيئًا عن دلالات البايت التي تحتويها ، فإن المقارنة الافتراضية ليس لديها خيار سوى ترتيب المفاتيح بترتيب معجمي ، واللجوء إلى المقارنة بين البايت. استخدامه لترتيب الهياكل يشبه الحلاقة بفأس نحت. ومع ذلك ، في حالات بسيطة ، أجد هذه الطريقة مقبولة. البديل موصوف أدناه ، لكن هنا سألاحظ اثنين من مكابس متناثرة على طول الطريق.

أول شيء يجب مراعاته هو تمثيل الذاكرة لأنواع البيانات البدائية. لذلك ، على جميع أجهزة Apple ، يتم تخزين متغيرات الأعداد الصحيحة بالتنسيق يذكر Endian. هذا يعني أن البايت الأقل أهمية سيكون على اليسار ، ولن تتمكن من فرز الأعداد الصحيحة باستخدام المقارنة بين البايت. على سبيل المثال ، ستؤدي محاولة القيام بذلك باستخدام مجموعة من الأرقام من 0 إلى 511 إلى النتيجة التالية.

// value (hex dump)
000 (0000)
256 (0001)
001 (0100)
257 (0101)
...
254 (fe00)
510 (fe01)
255 (ff00)
511 (ff01)

لحل هذه المشكلة ، يجب تخزين الأعداد الصحيحة في المفتاح بتنسيق مناسب لمقارن البايت. ستساعد الوظائف من الأسرة على إجراء التحول الضروري. hton* (خاصه htons للأرقام مزدوجة البايت من المثال).

شكل تمثيل السلاسل في البرمجة ، كما تعلم ، هو الكل تاريخ. إذا كانت دلالات السلاسل ، وكذلك الترميز المستخدم لتمثيلها في الذاكرة ، يشير إلى أنه قد يكون هناك أكثر من بايت واحد لكل حرف ، فمن الأفضل التخلي على الفور عن فكرة استخدام المقارنة الافتراضية.

الشيء الثاني الذي يجب مراعاته هو مبادئ المحاذاة مترجم المجال الإنشائي. بسببها ، يمكن تكوين وحدات البايت التي تحتوي على قيم غير صالحة في الذاكرة بين الحقول ، والتي ، بالطبع ، تكسر فرز البايت. للقضاء على البيانات المهملة ، يجب عليك إما إعلان الحقول بترتيب محدد بدقة ، مع مراعاة قواعد المحاذاة ، أو استخدام السمة في إعلان الهيكل packed.

ترتيب المفتاح بواسطة مقارن خارجي

قد يتضح أن منطق المقارنة الرئيسي معقد للغاية بالنسبة للمقارن الثنائي. أحد الأسباب العديدة هو وجود المجالات الفنية داخل الهياكل. سأوضح حدوثها في مثال مفتاح مألوف لنا بالفعل لعنصر دليل.

typedef struct NodeKey {​
    EntityId parentId;​
    uint8_t type;​
    uint8_t nameBuffer[256];​
} NodeKey;

على الرغم من بساطته ، فإنه في الغالبية العظمى من الحالات يستهلك الكثير من الذاكرة. يبلغ حجم المخزن المؤقت للعنوان 256 بايت ، على الرغم من أن أسماء الملفات والمجلدات نادرًا ما تتجاوز 20-30 حرفًا.

أحد الأساليب القياسية لتحسين حجم السجل هو "قصه" ليلائم الحجم الفعلي. جوهرها هو أن محتويات جميع الحقول متغيرة الطول يتم تخزينها في مخزن مؤقت في نهاية الهيكل ، ويتم تخزين أطوالها في متغيرات منفصلة. وفقًا لهذا النهج ، فإن المفتاح NodeKey يتحول بالطريقة التالية.

typedef struct NodeKey {​
    EntityId parentId;​
    uint8_t type;​
    uint8_t nameLength;​
    uint8_t nameBuffer[256];​
} NodeKey;

علاوة على ذلك ، أثناء التسلسل ، لم يتم تحديد حجم البيانات sizeof الهيكل بأكمله ، وحجم جميع الحقول ثابت الطول بالإضافة إلى حجم الجزء المستخدم بالفعل من المخزن المؤقت.

MDB_val serialize(NodeKey * const key) {
    return MDB_val {
        .mv_size = offsetof(NodeKey, nameBuffer) + key->nameLength,
        .mv_data = (void *)key
    };
}

نتيجة لإعادة الهيكلة ، حصلنا على توفير كبير في المساحة التي تشغلها المفاتيح. ومع ذلك ، بسبب المجال التقني nameLength، لم يعد المقارن الثنائي الافتراضي مناسبًا للمقارنة الرئيسية. إذا لم نستبدله بآخر خاص بنا ، فسيكون طول الاسم عاملًا ذا أولوية في الفرز أكثر من الاسم نفسه.

يسمح LMDB لكل قاعدة بيانات أن يكون لها وظيفة مقارنة مفاتيح خاصة بها. يتم ذلك باستخدام الوظيفة mdb_set_compare بدقة قبل الافتتاح. لأسباب واضحة ، لا يمكن تغيير قاعدة البيانات طوال حياتها. عند الإدخال ، يتلقى المقارنة مفتاحين بتنسيق ثنائي ، وعند الإخراج يُرجع نتيجة المقارنة: أقل من (-1) ، أكبر من (1) أو يساوي (0). الكود الزائف لـ NodeKey يبدو مثل هذا.

int compare(MDB_val * const a, MDB_val * const b) {​
    NodeKey * const aKey = (NodeKey * const)a->mv_data;​
    NodeKey * const bKey = (NodeKey * const)b->mv_data;​
    return // ...
}​

طالما أن جميع المفاتيح في قاعدة البيانات من نفس النوع ، فمن القانوني أن يتم تحويل تمثيل البايت دون قيد أو شرط إلى نوع هيكل تطبيق المفتاح. هناك فارق بسيط واحد هنا ، ولكن سيتم مناقشته بشكل أقل قليلاً في القسم الفرعي "قراءة السجلات".

تسلسل القيمة

باستخدام مفاتيح السجلات المخزنة ، يعمل LMDB بشكل مكثف للغاية. تتم مقارنتها مع بعضها البعض في إطار أي عملية تطبيق ، ويعتمد أداء الحل بأكمله على سرعة المقارنة. في عالم مثالي ، يجب أن يكون المقارن الثنائي الافتراضي كافيًا لمقارنة المفاتيح ، ولكن إذا كان عليك حقًا استخدام مفاتيحك الخاصة ، فيجب أن يكون إجراء إلغاء تسلسل المفاتيح فيه بأسرع ما يمكن.

قاعدة البيانات ليست مهتمة بشكل خاص بجزء القيمة من السجل (القيمة). يتم تحويله من تمثيل بايت إلى كائن فقط عندما يكون مطلوبًا بالفعل بواسطة رمز التطبيق ، على سبيل المثال ، لعرضه على الشاشة. نظرًا لأن هذا نادر الحدوث نسبيًا ، فإن متطلبات سرعة هذا الإجراء ليست بالغة الأهمية ، وفي تنفيذه ، نتمتع بحرية أكبر في التركيز على الراحة. على سبيل المثال ، لإجراء تسلسل للبيانات الوصفية حول الملفات التي لم يتم تنزيلها بعد ، نستخدم NSKeyedArchiver.

NSData *data = serialize(object);​
MDB_val value = {​
    .mv_size = data.length,​
    .mv_data = (void *)data.bytes​
};

ومع ذلك ، هناك أوقات يكون فيها الأداء مهمًا. على سبيل المثال ، عند حفظ المعلومات الوصفية حول بنية ملف سحابة المستخدم ، فإننا نستخدم نفس تفريغ ذاكرة الكائن. إن أهم ما يميز مهمة إنشاء تمثيلهم المتسلسل هو حقيقة أن عناصر الدليل تم تشكيلها بواسطة تسلسل هرمي للفئة.

تألق وفقر قاعدة بيانات القيمة الأساسية LMDB في تطبيقات iOS

لتنفيذه بلغة C ، يتم نقل الحقول المحددة للورثة إلى هياكل منفصلة ، ويتم تحديد ارتباطها بالقاعدة الأولى من خلال مجال من نوع الاتحاد. يتم تحديد المحتوى الفعلي للاتحاد عبر السمة الفنية للنوع.

typedef struct NodeValue {​
    EntityId localId;​
    EntityType type;​
    union {​
        FileInfo file;​
        DirectoryInfo directory;​
    } info;​
    uint8_t nameLength;​
    uint8_t nameBuffer[256];​
} NodeValue;​

إضافة وتحديث الإدخالات

يمكن إضافة المفتاح المتسلسل والقيمة إلى المتجر. لهذا ، يتم استخدام الوظيفة mdb_put.

// key и value имеют тип MDB_val​
mdb_put(..., &key, &value, MDB_NOOVERWRITE);

في مرحلة التكوين ، يمكن السماح للمستودع أو منعه من تخزين سجلات متعددة بنفس المفتاح.إذا كان تكرار المفاتيح محظورًا ، فعند إدراج سجل ، يمكنك تحديد ما إذا كان تحديث سجل موجود بالفعل مسموحًا به أم لا. إذا كان من الممكن أن يحدث الاحتكاك فقط نتيجة خطأ في الكود ، فيمكنك التأمين ضده من خلال تحديد العلم NOOVERWRITE.

سجلات القراءة

وظيفة قراءة السجلات في LMDB هي mdb_get. إذا تم تمثيل زوج المفتاح والقيمة بهياكل تم إغراقها مسبقًا ، فسيبدو هذا الإجراء على هذا النحو.

NodeValue * const readNode(..., NodeKey * const key) {​
    MDB_val rawKey = serialize(key);​
    MDB_val rawValue;​
    mdb_get(..., &rawKey, &rawValue);​
    return (NodeValue * const)rawValue.mv_data;​
}

توضح القائمة المعروضة كيف أن التسلسل من خلال تفريغ الهياكل يسمح لك بالتخلص من التخصيصات الديناميكية ليس فقط عند الكتابة ، ولكن عند قراءة البيانات. مشتق من الوظيفة mdb_get ينظر المؤشر بالضبط إلى عنوان الذاكرة الظاهرية حيث تخزن قاعدة البيانات تمثيل بايت للكائن. في الواقع ، نحصل على نوع من ORM ، شبه مجاني يوفر سرعة عالية جدًا في قراءة البيانات. مع كل جمال النهج ، من الضروري تذكر العديد من الميزات المرتبطة به.

  1. بالنسبة للمعاملة التي تتم للقراءة فقط ، يُضمن أن يظل مؤشر بنية القيمة صالحًا فقط حتى يتم إغلاق المعاملة. كما ذكرنا سابقًا ، تظل صفحات B-tree التي يوجد عليها الكائن ، بفضل مبدأ النسخ عند الكتابة ، دون تغيير طالما أن هناك معاملة واحدة على الأقل تشير إليها. في الوقت نفسه ، بمجرد اكتمال المعاملة الأخيرة المرتبطة بها ، يمكن إعادة استخدام الصفحات للحصول على بيانات جديدة. إذا كان من الضروري للكائنات البقاء على قيد الحياة في المعاملة التي أنشأتها ، فلا يزال يتعين نسخها.
  2. بالنسبة لمعاملة readwrite ، سيكون المؤشر إلى قيمة البنية الناتجة صالحًا فقط حتى إجراء التعديل الأول (كتابة البيانات أو حذفها).
  3. على الرغم من أن الهيكل NodeValue ليست كاملة ، ولكنها مشذبة (انظر القسم الفرعي "ترتيب المفاتيح بواسطة مقارن خارجي") ، من خلال المؤشر ، يمكنك الوصول بسهولة إلى حقولها. الشيء الرئيسي هو عدم تأجيل ذلك!
  4. لا يمكنك بأي حال من الأحوال تعديل الهيكل من خلال المؤشر المستلم. يجب إجراء جميع التغييرات فقط من خلال الطريقة mdb_put. ومع ذلك ، مع كل الرغبة في القيام بذلك ، فإنه لن ينجح ، حيث يتم تعيين منطقة الذاكرة حيث يوجد هذا الهيكل في وضع القراءة فقط.
  5. أعد تعيين ملف إلى مساحة العنوان لعملية ما ، على سبيل المثال ، لزيادة الحد الأقصى لحجم التخزين باستخدام الوظيفة mdb_env_set_map_size يبطل تمامًا جميع المعاملات والكيانات ذات الصلة بشكل عام ومؤشرات لقراءة الكائنات بشكل خاص.

أخيرًا ، هناك ميزة أخرى خبيثة لدرجة أن الكشف عن جوهرها لا يتناسب مع نقطة واحدة أخرى. في الفصل المتعلق بالشجرة B ، قدمت مخططًا لتنظيم صفحاتها في الذاكرة. ويترتب على ذلك أن عنوان بداية المخزن المؤقت بالبيانات المتسلسلة يمكن أن يكون تعسفيًا تمامًا. وبسبب هذا ، تم الحصول على المؤشر لهم في الهيكل MDB_val ويلقي بمؤشر إلى هيكل غير محاذي بشكل عام. في الوقت نفسه ، تتطلب بنيات بعض الرقائق (في حالة iOS ، هذا هو armv7) أن يكون عنوان أي بيانات مضاعفًا لحجم كلمة الآلة ، أو بعبارة أخرى ، شهادة النظام (بالنسبة لـ armv7 ، هذا هو 32 بت). بمعنى آخر ، عملية مثل *(int *foo)0x800002 عليهم يساوي الهروب ويؤدي إلى الإعدام بالحكم EXC_ARM_DA_ALIGN. هناك طريقتان لتجنب مثل هذا المصير المحزن.

الأول هو نسخ البيانات إلى بنية محاذاة معروفة مسبقًا. على سبيل المثال ، في المقارنة المخصصة ، سينعكس هذا على النحو التالي.

int compare(MDB_val * const a, MDB_val * const b) {
    NodeKey aKey, bKey;
    memcpy(&aKey, a->mv_data, a->mv_size);
    memcpy(&bKey, b->mv_data, b->mv_size);
    return // ...
}

هناك طريقة بديلة تتمثل في إخطار المترجم مسبقًا بأن الهياكل التي تحتوي على مفتاح وقيمة قد لا يتم محاذاة باستخدام سمة aligned(1). يمكن أن يكون نفس التأثير على ARM لتحقيقه واستخدام السمة المعبأة. بالنظر إلى أنه يساهم أيضًا في تحسين المساحة التي يشغلها الهيكل ، يبدو لي أن هذه الطريقة هي الأفضل ، على الرغم من приводит لزيادة تكلفة عمليات الوصول إلى البيانات.

typedef struct __attribute__((packed)) NodeKey {
    uint8_t parentId;
    uint8_t type;
    uint8_t nameLength;
    uint8_t nameBuffer[256];
} NodeKey;

استعلامات النطاق

للتكرار على مجموعة من السجلات ، يوفر LMDB تجريدًا للمؤشر. دعونا نلقي نظرة على كيفية التعامل معها باستخدام مثال جدول يحتوي على بيانات تعريف سحابية للمستخدم مألوفة لدينا بالفعل.

كجزء من عرض قائمة الملفات في دليل ، تحتاج إلى العثور على جميع المفاتيح التي ترتبط بها الملفات والمجلدات الفرعية. في الأقسام الفرعية السابقة ، قمنا بفرز المفاتيح NodeKey بحيث يتم ترتيبها أولاً بواسطة معرف الدليل الأصلي. وبالتالي ، من الناحية الفنية ، يتم تقليل مهمة الحصول على محتويات المجلد إلى وضع المؤشر على الحد العلوي لمجموعة من المفاتيح ببادئة معينة ، متبوعًا بالتكرار إلى الحد الأدنى.

تألق وفقر قاعدة بيانات القيمة الأساسية LMDB في تطبيقات iOS

يمكنك العثور على الحد العلوي "على الجبهة" عن طريق البحث المتسلسل. للقيام بذلك ، يتم وضع المؤشر في بداية قائمة المفاتيح بأكملها في قاعدة البيانات ثم يتم زيادتها حتى يظهر المفتاح مع معرف الدليل الأصلي تحته. هذا النهج له عيبان واضحان:

  1. التعقيد الخطي للبحث ، على الرغم من أنه ، كما تعلم ، في الأشجار بشكل عام وفي شجرة B بشكل خاص ، يمكن إجراؤه في الوقت اللوغاريتمي.
  2. عبثًا ، يتم رفع جميع الصفحات التي تسبق الصفحة المطلوبة من الملف إلى الذاكرة الرئيسية ، وهو أمر مكلف للغاية.

لحسن الحظ ، يوفر LMDB API طريقة فعالة لوضع المؤشر مبدئيًا. للقيام بذلك ، تحتاج إلى تكوين مفتاح من المعروف أن قيمته أقل من أو تساوي المفتاح الموجود في الحد العلوي للفاصل الزمني. على سبيل المثال ، فيما يتعلق بالقائمة في الشكل أعلاه ، يمكننا عمل مفتاح في الحقل parentId سيساوي 2 ، وكل الباقي مملوء بالأصفار. يتم تغذية هذا المفتاح المعبأ جزئيًا إلى مدخلات الوظيفة mdb_cursor_get تشير إلى العملية MDB_SET_RANGE.

NodeKey upperBoundSearchKey = {​
    .parentId = 2,​
    .type = 0,​
    .nameLength = 0​
};​
MDB_val value, key = serialize(upperBoundSearchKey);​
MDB_cursor *cursor;​
mdb_cursor_open(..., &cursor);​
mdb_cursor_get(cursor, &key, &value, MDB_SET_RANGE);

إذا تم العثور على الحد العلوي لمجموعة المفاتيح ، فإننا نكررها حتى نلتقي أو نلتقي مع مفتاح آخر parentId، أو المفاتيح لن تنفد على الإطلاق.

do {​
    rc = mdb_cursor_get(cursor, &key, &value, MDB_NEXT);​
    // processing...​
} while (MDB_NOTFOUND != rc && // check end of table​
         IsTargetKey(key));    // check end of keys group​​

ما هو لطيف ، كجزء من التكرار باستخدام mdb_cursor_get ، لا نحصل على المفتاح فحسب ، بل نحصل أيضًا على القيمة. إذا كان من الضروري ، من أجل استيفاء شروط الاختيار ، التحقق ، من بين أشياء أخرى ، من الحقول من جزء القيمة في السجل ، فعندئذٍ يمكن الوصول إليها تمامًا بدون إيماءات إضافية.

4.3 نمذجة العلاقات بين الجداول

حتى الآن ، تمكنا من النظر في جميع جوانب تصميم قاعدة بيانات من جدول واحد والعمل معها. يمكننا القول أن الجدول عبارة عن مجموعة من السجلات المصنفة تتكون من أزواج مفتاح - قيمة من نفس النوع. إذا قمت بعرض مفتاح كمستطيل والقيمة المرتبطة به كمربع ، ستحصل على رسم تخطيطي مرئي لقاعدة البيانات.

â € <

تألق وفقر قاعدة بيانات القيمة الأساسية LMDB في تطبيقات iOS

ومع ذلك ، في الحياة الواقعية ، نادرًا ما يكون من الممكن الحصول على القليل من الدم. غالبًا ما يكون مطلوبًا في قاعدة البيانات ، أولاً ، وجود عدة جداول ، وثانيًا ، إجراء التحديدات فيها بترتيب مختلف عن المفتاح الأساسي. هذا القسم الأخير مخصص لقضايا إنشائها وربطها.

جداول الفهرس

يحتوي التطبيق السحابي على قسم "معرض". يعرض محتوى الوسائط من السحابة بأكملها ، مرتبة حسب التاريخ. من أجل التنفيذ الأمثل لمثل هذا التحديد ، بجانب الجدول الرئيسي ، تحتاج إلى إنشاء واحد آخر بنوع جديد من المفاتيح. سوف تحتوي على حقل بتاريخ إنشاء الملف ، والذي سيكون بمثابة معيار الفرز الأساسي. نظرًا لأن المفاتيح الجديدة تشير إلى نفس البيانات مثل المفاتيح الموجودة في الجدول الأساسي ، فإنها تسمى مفاتيح الفهرس. تم تمييزها باللون البرتقالي في الصورة أدناه.

تألق وفقر قاعدة بيانات القيمة الأساسية LMDB في تطبيقات iOS

من أجل فصل مفاتيح الجداول المختلفة عن بعضها البعض في نفس قاعدة البيانات ، تمت إضافة معرف حقل تقني إضافي إليها جميعًا. من خلال جعلها الأولوية القصوى للفرز ، سنقوم بتجميع المفاتيح أولاً حسب الجداول ، وداخل الجداول بالفعل - وفقًا لقواعدنا الخاصة.

يشير مفتاح الفهرس إلى نفس البيانات مثل المفتاح الأساسي. يعد التنفيذ المباشر لهذه الخاصية من خلال إقرانها بنسخة من جزء القيمة للمفتاح الأساسي دون المستوى الأمثل من عدة وجهات نظر في وقت واحد:

  1. من وجهة نظر المساحة المشغولة ، يمكن أن تكون البيانات الوصفية غنية جدًا.
  2. من وجهة نظر الأداء ، لأنه عند تحديث البيانات الوصفية للعقدة ، سيتعين عليك استبدال مفتاحين.
  3. من وجهة نظر دعم الكود ، بعد كل شيء ، إذا نسينا تحديث البيانات لأحد المفاتيح ، فسنحصل على خطأ خفي في عدم تناسق البيانات في التخزين.

بعد ذلك ، سننظر في كيفية القضاء على أوجه القصور هذه.

تنظيم العلاقات بين الجداول

هذا النمط مناسب تمامًا لربط جدول الفهرس بالجدول الرئيسي "مفتاح كقيمة". كما يوحي اسمه ، فإن جزء القيمة في سجل الفهرس هو نسخة من قيمة المفتاح الأساسي. يزيل هذا الأسلوب جميع العيوب المذكورة أعلاه المرتبطة بتخزين نسخة من جزء القيمة من السجل الأساسي. الرسم الوحيد هو أنه للحصول على القيمة من خلال مفتاح الفهرس ، تحتاج إلى إجراء استعلامين في قاعدة البيانات بدلاً من استعلام واحد. من الناحية التخطيطية ، يكون مخطط قاعدة البيانات الناتج كما يلي.

تألق وفقر قاعدة بيانات القيمة الأساسية LMDB في تطبيقات iOS

نمط آخر لتنظيم العلاقات بين الجداول هو "مفتاح فائض". جوهرها هو إضافة سمات إضافية إلى المفتاح ، والتي لا تكون ضرورية للفرز ، ولكن لإعادة إنشاء المفتاح المرتبط.ومع ذلك ، هناك أمثلة حقيقية لاستخدامه في تطبيق Mail.ru Cloud ، من أجل تجنب الغوص العميق في في سياق أطر عمل iOS المحددة ، سأقدم مثالًا وهميًا ، لكن أكثر قابلية للفهم.

لدى عملاء الأجهزة المحمولة السحابية صفحة تعرض جميع الملفات والمجلدات التي شاركها المستخدم مع أشخاص آخرين. نظرًا لوجود عدد قليل نسبيًا من هذه الملفات ، وهناك الكثير من المعلومات المحددة حول الدعاية المرتبطة بها (لمن يُمنح الوصول ، وما هي الحقوق ، وما إلى ذلك) ، فلن يكون من المنطقي تحميلها بجزء القيمة من السجل في الجدول الرئيسي. ومع ذلك ، إذا كنت ترغب في عرض هذه الملفات في وضع عدم الاتصال ، فلا يزال يتعين عليك تخزينها في مكان ما. الحل الطبيعي هو إنشاء جدول منفصل لذلك. في الرسم البياني أدناه ، يكون مفتاحه مسبوقًا بـ "P" ، ويمكن استبدال العنصر النائب "propname" بالقيمة الأكثر تحديدًا "المعلومات العامة".

تألق وفقر قاعدة بيانات القيمة الأساسية LMDB في تطبيقات iOS

يتم نقل جميع البيانات الوصفية الفريدة ، من أجل إنشاء الجدول الجديد ، إلى جزء القيمة في السجل. في الوقت نفسه ، لا أريد تكرار البيانات حول الملفات والمجلدات المخزنة بالفعل في الجدول الرئيسي. بدلاً من ذلك ، تتم إضافة البيانات المكررة إلى المفتاح "P" في شكل حقلي "معرف العقدة" و "الطابع الزمني". بفضلهم ، يمكنك إنشاء مفتاح فهرس ، يمكنك من خلاله الحصول على المفتاح الأساسي ، والذي يمكنك أخيرًا من خلاله الحصول على البيانات الوصفية للعقدة.

خاتمة

نقوم بتقييم نتائج تنفيذ LMDB بشكل إيجابي. بعد ذلك ، انخفض عدد مرات تجميد التطبيق بنسبة 30٪.

تألق وفقر قاعدة بيانات القيمة الأساسية LMDB في تطبيقات iOS

وجدت نتائج العمل المنجز استجابة من خارج فريق iOS. حاليًا ، تحول أحد أقسام "الملفات" الرئيسية في تطبيق Android أيضًا إلى استخدام LMDB ، وهناك أجزاء أخرى في الطريق. كانت لغة C ، التي يتم فيها تنفيذ تخزين قيمة المفتاح ، مساعدة جيدة من أجل جعل التطبيق ملزمًا في البداية حوله عبر النظام الأساسي بلغة C ++. للحصول على اتصال سلس لمكتبة C ++ الناتجة مع رمز النظام الأساسي في Objective-C و Kotlin ، تم استخدام منشئ الكود جينني من Dropbox ، ولكن هذه قصة أخرى.

المصدر: www.habr.com

إضافة تعليق