مرحبا يا هبر! مع هذه المقالة أبدأ مجموعة من المسلسلات (أو سلسلة من المجموعات؟ - في كلمة ، الفكرة رائعة) حول البنية الداخلية لـ PostgreSQL.
ستستند المواد إلى
دورات تدريبية (باللغة الروسية) على الإدارة التي
أنشأناها أنا بافل
بلوزانوف . لا يحب الجميع مشاهدة الفيديو (أنا بالتأكيد لا أحب ذلك) ، وقراءة الشرائح ، حتى مع التعليقات ، ليست جيدة على الإطلاق.
لسوء الحظ ، فإن الدورة التدريبية الوحيدة المتاحة باللغة الإنجليزية في الوقت الحالي هي مقدمة ليومين لـ PostgreSQL 11 .
بطبيعة الحال ، فإن المقالات لن تكون بالضبط نفس محتوى الدورات. سوف أتحدث فقط عن كيفية تنظيم كل شيء ، وإهمال الإدارة نفسها ، لكنني سأحاول القيام بذلك بمزيد من التفصيل وبشكل أكثر شمولاً. وأعتقد أن المعرفة مثل هذه مفيدة لمطور التطبيق بقدر ما هي مفيدة للمسؤول.
سأستهدف أولئك الذين لديهم بالفعل بعض الخبرة في استخدام PostgreSQL وعلى الأقل بشكل عام فهم ما هو. سيكون النص صعبًا جدًا بالنسبة للمبتدئين. على سبيل المثال ، لن أقول كلمة حول كيفية تثبيت PostgreSQL وتشغيل psql.
لا تختلف الأشياء المعنية كثيرًا من إصدار إلى آخر ، لكنني سأستخدم الإصدار 11 من الفانيليا PostgreSQL الحالي.
تتناول السلسلة الأولى القضايا المتعلقة بالعزلة وتزامن التحويل ، وخطة السلسلة كما يلي:
- العزلة كما يفهمها المعيار و PostgreSQL (هذه المقالة).
- الشوك ، الملفات ، الصفحات - ما يحدث على المستوى المادي.
- إصدارات الصف والمعاملات الافتراضية والمعاملات الفرعية.
- لقطات البيانات وضوح إصدارات الصف ؛ أفق الحدث.
- فراغ في الصفحة والتحديثات الساخنة .
- فراغ عادي .
- Autov Vacuum .
- ملفوفة المعاملة و التجميد .
قبالة نذهب!
وقبل أن نبدأ ، أود أن أشكر Elena Indrupskaya على ترجمة المقالات إلى اللغة الإنجليزية.
ما هي العزلة ولماذا هي مهمة؟
من المحتمل أن يكون الجميع على الأقل على دراية بوجود المعاملات ، وقد صادفوا اختصارًا ACID ، واستمعوا إلى مستويات العزل. لكن ما زلنا نواجه الرأي القائل بأن هذا يتعلق بالنظرية ، وهو أمر غير ضروري في الممارسة. لذلك ، سأقضي بعض الوقت في محاولة لشرح سبب أهمية ذلك.
من غير المرجح أن تكون سعيدًا إذا حصل تطبيق ما على بيانات غير صحيحة من قاعدة البيانات أو إذا كان التطبيق يكتب بيانات غير صحيحة إلى قاعدة البيانات.
ولكن ما هي البيانات "الصحيحة"؟ من المعروف أنه يمكن إنشاء
قيود التكامل ، مثل NOT NULL أو UNIQUE ، على مستوى قاعدة البيانات. إذا كانت البيانات تفي دائمًا بقيود التكامل (وهذا هو الحال لأن قواعد بيانات نظام إدارة قواعد البيانات تضمنها) ، فهي جزء لا يتجزأ.
هل
صحيح ومتكامل نفس الأشياء؟ ليس بالضبط لا يمكن تحديد جميع القيود على مستوى قاعدة البيانات. بعض القيود معقدة للغاية ، على سبيل المثال ، التي تغطي عدة جداول في وقت واحد. وحتى إذا كان من الممكن تحديد قيود بشكل عام في قاعدة البيانات ، ولكن لسبب ما لم يكن الأمر كذلك ، فهذا لا يعني أنه يمكن انتهاك القيد.
لذا ،
فالصحة أقوى من
النزاهة ، لكننا لا نعرف بالضبط ما يعنيه هذا. ليس لدينا سوى الاعتراف بأن "المعيار الذهبي" للصحة هو تطبيق ، كما نود أن نصدق ، هو مكتوب
بشكل صحيح ولا يعمل أبداً. في أي حال ، إذا كان التطبيق لا ينتهك النزاهة ، ولكنه ينتهك الصواب ، فلن يعرف نظام إدارة قواعد البيانات (DBMS) بشأنه ولن يقبض على التطبيق "متخلف".
كذلك سوف نستخدم مصطلح
الاتساق للإشارة إلى الصحة.
دعنا ، مع ذلك ، نفترض أن التطبيق ينفذ فقط التسلسلات الصحيحة للمشغلين. ما هو دور DBMS إذا كان التطبيق صحيحًا كما هو؟
أولاً ، اتضح أن التسلسل الصحيح للمشغلين يمكن أن يكسر مؤقتًا تناسق البيانات ، وهذا أمر غريب على نحو غريب. والمثال الذي تم اختراقه ولكنه واضح هو تحويل الأموال من حساب إلى آخر. قد تبدو قاعدة التناسق على هذا
النحو :
لا يغير التحويل أبدًا المبلغ الإجمالي للأموال على الحسابات (يصعب تحديد هذه القاعدة في SQL كقيد للتكامل ، لذلك فهي موجودة على مستوى التطبيق وتكون غير مرئية لـ DBMS). يتكون التحويل من عمليتين: الأولى تقلل الأموال في حساب واحد ، والثانية - تزيدها من جهة أخرى. تحطّم العملية الأولى تناسق البيانات ، بينما تقوم العملية الثانية باستعادتها.
التمرين الجيد هو تنفيذ القاعدة المذكورة أعلاه على مستوى قيود النزاهة.
ماذا لو تم تنفيذ العملية الأولى والثانية ليست كذلك؟ في الواقع ، من دون الكثير من اللغط: أثناء العملية الثانية قد يحدث انقطاع في الكهرباء ، تعطل الخادم ، القسمة على الصفر - أيا كان. من الواضح أن التناسق سيتم كسره ، وهذا غير مسموح به. بشكل عام ، من الممكن حل هذه المشكلات على مستوى التطبيق ، ولكن على حساب الجهود الهائلة ؛ ومع ذلك ، لحسن الحظ ، ليس من الضروري: يتم ذلك بواسطة DBMS. ولكن للقيام بذلك ، يجب أن يعرف نظام إدارة قواعد البيانات (DBMS) أن العمليتين كليتين لا تتجزأ. وهذا هو ،
الصفقة .
اتضح أنه مثير للاهتمام: لأن قاعدة بيانات إدارة قواعد البيانات تعرف أن العمليات تشكل معاملة ، فهي تساعد في الحفاظ على الاتساق من خلال ضمان أن تكون المعاملات ذرية ، وهي تفعل ذلك دون معرفة أي شيء عن قواعد التناسق المحددة.
ولكن هناك نقطة ثانية أكثر دقة. بمجرد ظهور العديد من المعاملات المتزامنة في النظام ، والتي تكون صحيحة تمامًا بشكل منفصل ، فقد تفشل في العمل معًا بشكل صحيح. هذا بسبب اختلاط ترتيب العمليات: لا يمكنك افتراض أن جميع عمليات إحدى المعاملات يتم تنفيذها أولاً ، ثم جميع عمليات الأخرى.
مذكرة حول التزامن. في الواقع ، يمكن تشغيل المعاملات في وقت واحد على نظام مع معالج متعدد النواة ، صفيف القرص ، إلخ. لكن نفس المنطق ينطبق على خادم ينفذ الأوامر بالتسلسل ، في وضع مشاركة الوقت: خلال دورات معينة على مدار الساعة ، يتم تنفيذ معاملة واحدة ، وخلال دورات معينة أخرى يتم تنفيذ العملية الأخرى. في بعض الأحيان يتم استخدام مصطلح التنفيذ
المتزامن للتعميم.
تسمى المواقف التي تعمل فيها المعاملات الصحيحة مع بعضها بشكل غير صحيح
الحالات الشاذة للتنفيذ المتزامن.
على سبيل المثال البسيط: إذا أراد أحد التطبيقات الحصول على بيانات صحيحة من قاعدة البيانات ، فلا يجب عليه ، على الأقل ، الاطلاع على تغييرات المعاملات الأخرى غير الملتزم بها. خلاف ذلك ، لا يمكنك فقط الحصول على بيانات غير متسقة ، ولكن يمكنك أيضًا رؤية شيء لم يكن موجودًا في قاعدة البيانات (إذا تم إلغاء المعاملة). وتسمى هذه الشذوذ
قراءة قذرة .
هناك حالات شاذة أخرى أكثر تعقيدًا سنتعامل معها لاحقًا.
من المستحيل بالتأكيد تجنب التنفيذ المتزامن: وإلا ، فما نوع الأداء الذي يمكن أن نتحدث عنه؟ لكن لا يمكنك العمل مع بيانات غير صحيحة.
ومرة أخرى يأتي نظام إدارة قواعد البيانات الإنقاذية. يمكنك إجراء المعاملات
كما لو كانت بالتتابع ،
كما لو كانت واحدة تلو الأخرى. وبعبارة أخرى -
معزولة عن بعضها البعض. في الواقع ، يمكن لـ DBMS إجراء عمليات مختلطة ، ولكن تأكد من أن نتيجة التنفيذ المتزامن ستكون هي نفسها نتيجة لبعض عمليات الإعدام المتتالية المحتملة. وهذا يلغي أي الشذوذ ممكن.
لذلك وصلنا إلى التعريف:
المعاملة هي مجموعة من العمليات التي يؤديها تطبيق ينقل قاعدة بيانات من حالة صحيحة إلى حالة صحيحة أخرى (الاتساق) ، بشرط أن تكون المعاملة مكتملة (atomicity) ودون تدخل من المعاملات الأخرى (العزلة).
يوحد هذا التعريف الأحرف الثلاثة الأولى من اختصار ACID. ترتبط ارتباطًا وثيقًا ببعضها البعض بحيث لا معنى للنظر في أحدهما دون الآخرين. في الواقع ، من الصعب أيضًا فصل الحرف D (المتانة). في الواقع ، عندما يتعطل النظام ، لا يزال لديه تغييرات في المعاملات غير الملتزم بها ، والتي تحتاج إلى القيام بشيء ما لاستعادة تناسق البيانات.
كان كل شيء على ما يرام ، ولكن تنفيذ العزل التام هو مهمة صعبة تقنيًا تستلزم تقليل إنتاجية النظام. لذلك ، في الممارسة العملية في كثير من الأحيان (وليس دائما ، ولكن دائما تقريبا) يتم استخدام العزلة الضعيفة ، والتي تمنع بعض ، ولكن ليس كل الشذوذ. هذا يعني أن جزءًا من العمل لضمان صحة البيانات يقع على التطبيق. لهذا السبب بالذات ، من المهم للغاية فهم مستوى العزل المستخدم في النظام ، وما يضمنه وما لا يقدمه ، وكيفية كتابة التعليمات البرمجية الصحيحة في ظل هذه الظروف.
مستويات العزل والشذوذ في معيار SQL
وصف معيار SQL منذ فترة طويلة أربعة مستويات من العزلة. يتم تعريف هذه المستويات عن طريق سرد الحالات الشاذة المسموح بها أو غير المسموح بها عندما يتم تنفيذ المعاملات في وقت واحد على هذا المستوى. لذلك ، للحديث عن هذه المستويات ، من الضروري التعرف على الحالات الشاذة.
أشدد على أننا نتحدث في هذا الجزء عن المعيار ، أي عن النظرية ، التي تستند إليها الممارسة إلى حد كبير ، ولكنها تتباعد منها في الوقت نفسه. لذلك ، كل الأمثلة هنا هي المضاربة. سوف يستخدمون نفس العمليات على حسابات العملاء: هذا أمر توضيحي ، على الرغم من أنه من المسلم به أنه لا علاقة له بكيفية تنظيم العمليات المصرفية في الواقع.
تحديث الخسارة
لنبدأ
بالتحديث المفقود . تحدث هذه الحالة الشاذة عندما تقرأ معاملتان نفس صف الجدول ، ثم تقوم إحدى المعاملات بتحديث ذلك الصف ، ثم تقوم المعاملة الثانية أيضًا بتحديث نفس الصف دون مراعاة التغييرات التي تم إجراؤها بواسطة المعاملة الأولى.
على سبيل المثال ، ستعمل معاملتان على زيادة المبلغ على نفس الحساب بمقدار ₽ 100 (₽ هي علامة العملة بالروبل الروسي). تقوم المعاملة الأولى بقراءة القيمة الحالية (and1000) ثم تقوم المعاملة الثانية بقراءة نفس القيمة. المعاملة الأولى تزيد من المبلغ (هذا يعطي ₽1100) وتكتب هذه القيمة. تعمل المعاملة الثانية بنفس الطريقة: تحصل على نفس القيمة ₽1100 وتكتب هذه القيمة. ونتيجة لذلك ، فقد العميل ₽ 100.
لا يسمح المعيار بالتحديثات المفقودة في أي مستوى من العزل.
قراءة قذرة وقراءة غير ملتزم بها
القراءة القذرة هي ما تعرفنا عليه بالفعل. يحدث هذا الشذوذ عندما تقرأ المعاملة التغييرات التي لم يتم الالتزام بها بعد من خلال معاملة أخرى.
على سبيل المثال ، تقوم المعاملة الأولى بنقل جميع الأموال من حساب العميل إلى حساب آخر ، ولكنها لا تلزم التغيير. تقوم معاملة أخرى بقراءة رصيد الحساب ، للحصول على ₽0 ، وترفض سحب النقود إلى العميل ، على الرغم من إحباط المعاملة الأولى وإرجاء تغييراتها ، لذلك لم تكن القيمة 0 موجودة في قاعدة البيانات.
يسمح المعيار بقراءات متسخة على مستوى القراءة غير الملتزم بها.
غير قابلة للتكرار قراءة وقراءة ارتكبت
يحدث شذوذ
قراءة غير قابل للتكرار عندما تقوم المعاملة بقراءة نفس الصف مرتين ، وفي ما بين القراءات ، تقوم المعاملة الثانية بتعديل (أو حذف) ذلك الصف وتنفيذ التغييرات. ثم الصفقة الأولى ستحصل على نتائج مختلفة.
على سبيل المثال ، دع قاعدة التناسق
تحظر المبالغ السلبية على حسابات العملاء . ستعمل الصفقة الأولى على تقليل المبلغ على الحساب بمقدار 100 ₽. إنه يتحقق من القيمة الحالية ، ويحصل على ₽ 1000 ويقرر أن الانخفاض ممكن. في نفس الوقت ، تقوم المعاملة الثانية بتخفيض المبلغ على الحساب إلى صفر وتنفذ التغييرات. إذا كانت المعاملة الأولى قد أعادت فحص المبلغ ، فسوف تحصل على 0 (ولكن قررت بالفعل خفض القيمة ، والحساب "ينتقل إلى اللون الأحمر").
يسمح المعيار بقراءات غير قابلة للتكرار على مستويات القراءة غير الملتزم بها والقراءة. ولكن قراءة ارتكبت لا يسمح القراءات القذرة.
قراءة الوهمية والقراءة المتكررة
تحدث
القراءة الوهمية عندما تقرأ المعاملة مجموعة من الصفوف بنفس الشرط مرتين ، وفيما بين القراءات ، تضيف المعاملة الثانية صفوفًا تلبي هذا الشرط (وتنفذ التغييرات). ثم ستحصل المعاملة الأولى على مجموعات مختلفة من الصفوف.
على سبيل المثال ، اسمح لقاعدة الاتساق
بمنع العميل من امتلاك أكثر من 3 حسابات . تفتح الصفقة الأولى حسابًا جديدًا ، وتتحقق من العدد الحالي للحسابات (على سبيل المثال ، 2) ، وتقرر إمكانية فتح الحساب. في الوقت نفسه ، تفتح المعاملة الثانية أيضًا حسابًا جديدًا للعميل وتنفذ التغييرات. الآن إذا قامت المعاملة الأولى بإعادة التحقق من الرقم ، فستحصل على 3 (لكنها تفتح بالفعل حسابًا آخر ، ويبدو أن العميل لديه 4 منهم).
يسمح المعيار بقراءة الوهمية في مستويات القراءة غير الملتزم بها ، والقراءة ، والتكرار المقروء. ومع ذلك ، لا يُسمح بالقراءة غير القابلة للتكرار في مستوى تكرار القراءة.
غياب الشذوذ والتسلسل
يحدد المعيار مستوى آخر - التسلسل - والذي لا يسمح بأي شذوذ. وهذا ليس هو نفسه بالنسبة إلى منع التحديثات المفقودة والقراءات القذرة أو غير القابلة للتكرار أو الوهمية.
الشيء هو أن هناك الكثير من الحالات الشاذة المعروفة أكثر من المدرجة في المعيار وأيضاً عدد غير معروف من الحالات غير المعروفة.
يجب أن يمنع مستوى Serializable
كل الحالات الشاذة. هذا يعني أنه في هذا المستوى ، لا يحتاج مطور التطبيق إلى التفكير في التنفيذ المتزامن. إذا كانت المعاملات تنفذ تسلسلًا صحيحًا للمشغلين الذين يعملون بشكل منفصل ، فستكون البيانات متسقة أيضًا عند تنفيذ هذه المعاملات في وقت واحد.
جدول ملخص
الآن يمكننا توفير جدول معروف. ولكن هنا يتم إضافة العمود الأخير ، وهو مفقود من المعيار ، للوضوح.
لماذا بالضبط هذه الحالات الشاذة؟
لماذا لا تدرج القائمة القياسية سوى عدد قليل من الحالات الشاذة المحتملة ، ولماذا هم بالضبط؟
لا أحد يبدو أنه يعرف ذلك بالتأكيد. ولكن من الواضح أن هذه الممارسة تسبق النظرية ، لذلك فمن المحتمل في ذلك الوقت (وفقًا لمعيار SQL: 92) أن هناك حالات شاذة أخرى لم يتم التفكير فيها.
بالإضافة إلى ذلك ، كان من المفترض أن العزلة يجب أن تبنى على الأقفال. الفكرة وراء
بروتوكول القفل ثنائي الطور (2PL) المستخدم على نطاق واسع هي أنه أثناء التنفيذ ، تغلق الصفقة الصفوف التي تعمل بها وتصدر الأقفال عند الانتهاء. تبسيطًا كبيرًا ، كلما زاد عدد عمليات التأمين التي تمارسها ، كان عزلها أفضل عن المعاملات الأخرى. ولكن أداء النظام يعاني أيضًا أكثر من ذلك ، لأنه بدلاً من العمل معًا ، تبدأ المعاملات في الانتظار في الصفوف نفسها.
إحساسي هو أنه مجرد عدد الأقفال المطلوبة ، وهو ما يفسر الفرق بين مستويات عزل المعيار.
إذا قامت إحدى المعاملات بإغلاق الصفوف المراد تعديلها من التحديث ، ولكن ليس من القراءة ، فسنحصل على مستوى القراءة غير الملتزم بها: لا يُسمح بالتغييرات المفقودة ، ولكن يمكن قراءة البيانات غير الملتزم بها.
إذا أغلقت المعاملة الصفوف المراد تعديلها من كل من القراءة والتحديث ، فسنحصل على مستوى "قراءة ملتزم": لا يمكنك قراءة بيانات غير ملتزم بها ، لكن يمكنك الحصول على قيمة مختلفة (قراءة غير قابلة للتكرار) عند الوصول إلى الصف مرة أخرى.
إذا أغلقت المعاملة الصفوف المراد قراءتها وتعديلها وكلاهما من القراءة والتحديث ، فسنحصل على مستوى القراءة المكررة: إعادة قراءة الصف ستعود بنفس القيمة.
ولكن هناك مشكلة في Serializable: لا يمكنك قفل صف غير موجود حتى الآن. لذلك ، لا تزال القراءة الوهمية ممكنة: قد تضيف معاملة أخرى (ولكن لا تحذف) صفًا يفي بشروط الاستعلام الذي تم تنفيذه مسبقًا ، وسيتم إدراج ذلك الصف في إعادة التحديد.
لذلك ، لتنفيذ مستوى Serializable ، لا تكفي الأقفال العادية - تحتاج إلى قفل الشروط (المسند) بدلاً من الصفوف. لذلك ، كانت تسمى هذه الأقفال
المسند . لقد تم اقتراحها في عام 1976 ، ولكن قابليتها للتطبيق العملي مقيدة بشروط بسيطة إلى حد ما من الواضح أنه من الواضح كيف تنضم إلى مسندتين مختلفتين. حسب علمي ، لم يتم تطبيق هذه الأقفال في أي نظام حتى الآن.
مستويات العزلة في بوستجرس
بمرور الوقت ، تم استبدال بروتوكولات إدارة المعاملات القائمة على القفل ببروتوكول عزل اللقطات (SI). فكرتها هي أن كل معاملة تعمل مع لقطة متسقة للبيانات في وقت معين ، وفقط تلك التغييرات تدخل في اللقطة التي تم الالتزام بها قبل إنشائها.
هذه العزلة تلقائيا يمنع القراءات القذرة. بشكل رسمي ، يمكنك تحديد مستوى "قراءة غير ملتزم بها" في PostgreSQL ، لكنه سيعمل بنفس طريقة قراءة "الالتزام" تمامًا. لذلك ، كذلك لن نتحدث عن مستوى القراءة غير الملتزم بها على الإطلاق.
ينفذ PostgreSQL متغيرًا
متعدد التحويلات لهذا البروتوكول. فكرة التزامن المتعدد هي أنه يمكن أن تتعايش إصدارات متعددة من نفس الصف في نظام إدارة قواعد البيانات. يتيح لك ذلك إنشاء لقطة للبيانات باستخدام الإصدارات الموجودة واستخدام الحد الأدنى من الأقفال. في الواقع ، يتم تأمين التغييرات اللاحقة على نفس الصف فقط. يتم تنفيذ جميع العمليات الأخرى في وقت واحد: المعاملات الكتابة لا تقفل أبدا المعاملات للقراءة فقط ، والمعاملات للقراءة فقط لا تغلق أي شيء.
باستخدام لقطات البيانات ، تكون العزلة في PostgreSQL أكثر صرامة مما هو مطلوب في المعيار: لا يسمح مستوى القراءة المتكررة ليس فقط القراءة غير القابلة للتكرار ، ولكن أيضًا القراءة الوهمية (على الرغم من أنها لا توفر عزلًا كاملاً). وهذا يتحقق دون فقدان الكفاءة.
سنتحدث في المقالات التالية عن كيفية تنفيذ التزامن المتعدد "تحت الغطاء" ، والآن سننظر بالتفصيل في كل مستوى من المستويات الثلاثة بعين المستخدم (كما تعلمون ، يتم إخفاء الأكثر إثارة للاهتمام وراء "الحالات الشاذة الأخرى "). للقيام بذلك ، دعونا ننشئ جدول حسابات. يمتلك كل من Alice و Bob حوالي 1000 لكل منهما ، لكن لدى Bob حسابان مفتوحان:
=> CREATE TABLE accounts( id integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, number text UNIQUE, client text, amount numeric ); => INSERT INTO accounts VALUES (1, '1001', 'alice', 1000.00), (2, '2001', 'bob', 100.00), (3, '2002', 'bob', 900.00);
قراءة ملتزمة
غياب القراءة القذرة
من السهل التأكد من أنه لا يمكن قراءة البيانات القذرة. نبدأ المعاملة. بشكل افتراضي ، سوف يستخدم مستوى عزل قراءة ملتزم:
=> BEGIN; => SHOW transaction_isolation;
transaction_isolation ----------------------- read committed (1 row)
بتعبير أدق ، يتم تعيين المستوى الافتراضي بواسطة المعلمة ، والتي يمكن تغييرها إذا لزم الأمر:
=> SHOW default_transaction_isolation;
default_transaction_isolation ------------------------------- read committed (1 row)
لذلك ، في معاملة مفتوحة ، نقوم بسحب الأموال من الحساب ، لكننا لا نلتزم بإجراء التغييرات. ترى المعاملة التغييرات الخاصة بها:
=> UPDATE accounts SET amount = amount - 200 WHERE id = 1; => SELECT * FROM accounts WHERE client = 'alice';
id | number | client | amount ----+--------+--------+-------- 1 | 1001 | alice | 800.00 (1 row)
في الجلسة الثانية ، سنبدأ معاملة أخرى بنفس مستوى قراءة الالتزام. للتمييز بين المعاملات ، سيتم وضع مسافة بادئة لأوامر المعاملة الثانية مع وضع علامة على شريط.
لتكرار الأوامر المذكورة أعلاه (وهو أمر مفيد) ، تحتاج إلى فتح محطتين وتشغيل psql في كل واحدة. في المحطة الأولى ، يمكنك إدخال أوامر إحدى المعاملات ، وفي الثانية - أوامر الأخرى.
| => BEGIN; | => SELECT * FROM accounts WHERE client = 'alice';
| id | number | client | amount | ----+--------+--------+--------- | 1 | 1001 | alice | 1000.00 | (1 row)
كما هو متوقع ، لا ترى المعاملة الأخرى تغييرات غير ملتزم بها حيث لا يُسمح بالقراءات المتسخة.
قراءة غير قابلة للتكرار
والآن ، دع المعاملة الأولى تلتزم بالتغييرات والثانية تعيد تنفيذ نفس الاستعلام.
=> COMMIT;
| => SELECT * FROM accounts WHERE client = 'alice';
| id | number | client | amount | ----+--------+--------+-------- | 1 | 1001 | alice | 800.00 | (1 row)
| => COMMIT;
يحصل الاستعلام بالفعل على بيانات جديدة - وهذا هو الشذوذ في
القراءة غير القابل للتكرار ، والذي يُسمح به على مستوى "قراءة ملتزم".
الاستنتاج العملي : في المعاملة ، لا يمكنك اتخاذ قرارات بناءً على البيانات التي يقرأها المشغل السابق لأن الأشياء يمكن أن تتغير بين تنفيذ المشغلين. فيما يلي مثال على اختلاف أشكاله في كثير من الأحيان في رمز التطبيق بحيث يُعتبر مضادًا كلاسيكيًا:
IF (SELECT amount FROM accounts WHERE id = 1) >= 1000 THEN UPDATE accounts SET amount = amount - 1000 WHERE id = 1; END IF;
خلال الوقت الذي يمتد بين التدقيق والتحديث ، يمكن للمعاملات الأخرى تغيير حالة الحساب بأي طريقة ، لذا فإن هذا "التحقق" يؤمن من لا شيء. من المريح أن نتخيل أنه بين مشغلي معاملة واحدة ، يمكن لأي مشغلين آخرين للمعاملات الأخرى "إسفين" ، على سبيل المثال ، على النحو التالي:
IF (SELECT amount FROM accounts WHERE id = 1) >= 1000 THEN
إذا كان بالإمكان إفساد كل شيء عن طريق إعادة ترتيب العوامل ، فسيتم كتابة الرمز بطريقة غير صحيحة. ولا تخدع نفسك أن مثل هذه المصادفة لن تحدث - بل ستحدث بالتأكيد.
ولكن كيف تكتب الكود بشكل صحيح؟ الخيارات تميل إلى أن تكون على النحو التالي:
- ليس لكتابة الرمز.
هذه ليست مزحة. على سبيل المثال ، في هذه الحالة ، يتحول التحقق بسهولة إلى قيد النزاهة:
ALTER TABLE accounts ADD CHECK amount >= 0;
لا يلزم إجراء اختبارات الآن: ما عليك سوى إجراء العملية ، وإذا لزم الأمر ، قم بمعالجة الاستثناء الذي سيحدث عند محاولة انتهاك السلامة.
- لاستخدام عبارة SQL واحدة.
تنشأ مشاكل التناسق لأنه في الفترة الزمنية بين المشغلين يمكن إكمال معاملة أخرى ، مما سيؤدي إلى تغيير البيانات المرئية. وإذا كان هناك عامل واحد ، فلا توجد فواصل زمنية.
يحتوي PostgreSQL على تقنيات كافية لحل المشكلات المعقدة باستخدام عبارة SQL واحدة. دعونا نلاحظ تعبيرات الجدول الشائعة (CTE) ، والتي ، من بينها ، يمكنك استخدام عبارات INSERT / UPDATE / DELETE ، بالإضافة إلى عبارة INSERT ON CONFLICT ، والتي تنفذ منطق "insert ، ولكن إذا كان الصف موجودًا بالفعل ، تحديث "في بيان واحد.
- أقفال مخصصة.
الملاذ الأخير هو تعيين قفل حصري يدويًا على جميع الصفوف الضرورية (SELECT FOR UPDATE) أو حتى على الجدول بأكمله (LOCK TABLE). يعمل هذا دائمًا ، لكنه يلغي فوائد التزامن المتعدد: سيتم تنفيذ بعض العمليات بالتسلسل بدلاً من التنفيذ المتزامن.
قراءة غير متناسقة
قبل الانتقال إلى المستوى التالي من العزلة ، عليك أن تعترف بأنه ليس كل شيء بسيطًا كما يبدو. يعد تطبيق PostgreSQL أمرًا يسمح بوجود حالات شاذة أخرى أقل شهرة لا يتم تنظيمها وفقًا للمعايير.
لنفترض أن المعاملة الأولى بدأت في تحويل الأموال من حساب Bob إلى حساب آخر:
=> BEGIN; => UPDATE accounts SET amount = amount - 100 WHERE id = 2;
في الوقت نفسه ، تحسب معاملة أخرى رصيد بوب ، ويتم الحساب في حلقة على جميع حسابات بوب. في الواقع ، تبدأ المعاملة بالحساب الأول (ومن الواضح أن الحالة السابقة):
| => BEGIN; | => SELECT amount FROM accounts WHERE id = 2;
| amount | -------- | 100.00 | (1 row)
في هذه المرحلة الزمنية ، تتم المعاملة الأولى بنجاح:
=> UPDATE accounts SET amount = amount + 100 WHERE id = 3; => COMMIT;
والآخر يقرأ حالة الحساب الثاني (ويرى بالفعل القيمة الجديدة):
| => SELECT amount FROM accounts WHERE id = 3;
| amount | --------- | 1000.00 | (1 row)
| => COMMIT;
لذلك ، حصلت المعاملة الثانية على 001100 ، أي بيانات غير صحيحة. وهذا هو الشذوذ
قراءة غير متناسقة .
كيف يمكن تجنب مثل هذا الوضع الشاذ أثناء البقاء في مستوى "قراءة ملتزم"؟ بالطبع ، استخدم مشغل واحد. على سبيل المثال:
SELECT sum(amount) FROM accounts WHERE client = 'bob';
أكدت هنا أن رؤية البيانات لا يمكن أن تتغير إلا بين المشغلين ، ولكن هل هذا واضح للغاية؟ وإذا استغرق الاستعلام وقتًا طويلاً ، فهل يمكنه رؤية جزء من البيانات في حالة واحدة وجزء في حالة أخرى؟
دعونا تحقق. طريقة ملائمة للقيام بذلك هي إدراج تأخير قسري في المشغل عن طريق استدعاء وظيفة pg_sleep. المعلمة الخاصة به تحدد وقت التأخير بالثواني.
=> SELECT amount, pg_sleep(2) FROM accounts WHERE client = 'bob';
أثناء تنفيذ هذا المشغل ، نقوم بإعادة الأموال في معاملة أخرى:
| => BEGIN; | => UPDATE accounts SET amount = amount + 100 WHERE id = 2; | => UPDATE accounts SET amount = amount - 100 WHERE id = 3; | => COMMIT;
تظهر النتيجة أن المشغل يرى البيانات في الحالة التي كانت لديهم في وقت بدء تنفيذ المشغل. هذا صحيح بلا شك.
amount | pg_sleep ---------+---------- 0.00 | 1000.00 | (2 rows)
لكنها ليست بهذه البساطة هنا أيضا. يسمح لك PostgreSQL بتحديد الوظائف ، وتتمتع الوظائف بمفهوم
فئة التقلب . إذا تم استدعاء وظيفة VOLATILE في استعلام وتم تنفيذ استعلام آخر في تلك الوظيفة ، فسيشاهد الاستعلام الموجود داخل الوظيفة بيانات لا تتوافق مع البيانات الموجودة في الاستعلام الرئيسي.
=> CREATE FUNCTION get_amount(id integer) RETURNS numeric AS $$ SELECT amount FROM accounts a WHERE a.id = get_amount.id; $$ VOLATILE LANGUAGE sql;
=> SELECT get_amount(id), pg_sleep(2) FROM accounts WHERE client = 'bob';
| => BEGIN; | => UPDATE accounts SET amount = amount + 100 WHERE id = 2; | => UPDATE accounts SET amount = amount - 100 WHERE id = 3; | => COMMIT;
في هذه الحالة ، نحصل على بيانات غير صحيحة - ₽ 100 مفقودة:
get_amount | pg_sleep ------------+---------- 100.00 | 800.00 | (2 rows)
أؤكد أن هذا التأثير ممكن فقط على مستوى عزل القراءة الملتزم وفقط مع وظائف VOLATILE. المشكلة هي أنه افتراضياً ، يتم استخدام مستوى العزل هذا وفئة التقلبات هذه. لا تسقط في الفخ!
قراءة غير متسقة في مقابل التغييرات المفقودة
يمكننا أيضًا الحصول على قراءة غير متسقة داخل مشغل واحد أثناء التحديث ، على الرغم من ذلك بطريقة غير متوقعة إلى حد ما.
دعونا نرى ما يحدث عندما تحاول معاملتان تعديل نفس الصف. الآن لديه Bob ₽1000 على حسابين:
=> SELECT * FROM accounts WHERE client = 'bob';
id | number | client | amount ----+--------+--------+-------- 2 | 2001 | bob | 200.00 3 | 2002 | bob | 800.00 (2 rows)
نبدأ معاملة تقلل من رصيد بوب:
=> BEGIN; => UPDATE accounts SET amount = amount - 100 WHERE id = 3;
في الوقت نفسه ، في معاملة أخرى تستحق الفائدة على جميع حسابات العملاء برصيد إجمالي يساوي أو يزيد عن 1000::
| => UPDATE accounts SET amount = amount * 1.01 | WHERE client IN ( | SELECT client | FROM accounts | GROUP BY client | HAVING sum(amount) >= 1000 | );
تنفيذ مشغل UPDATE يتكون من جزأين. أولاً ، يتم تنفيذ SELECT بالفعل ، والذي يحدد الصفوف لتحديثها التي تلبي الشرط المناسب. نظرًا لأن التغيير في المعاملة الأولى غير ملتزم به ، لا يمكن للمعاملة الثانية رؤيته ، ولا يؤثر التغيير في اختيار الصفوف لتراكم الفوائد. حسنًا ، حسابات بوب تفي بالشرط وبمجرد تنفيذ التحديث ، يجب زيادة رصيده بمقدار ₽10.
المرحلة الثانية من التنفيذ هي تحديث الصفوف المحددة واحدة تلو الأخرى. هنا يتم إجبار المعاملة الثانية على "تعليق" لأن الصفوف ذات المعرف = 3 مؤمنة بالفعل بواسطة المعاملة الأولى.
وفي الوقت نفسه ، تلتزم المعاملة الأولى بالتغييرات:
=> COMMIT;
ماذا ستكون النتيجة؟
=> SELECT * FROM accounts WHERE client = 'bob';
id | number | client | amount ----+--------+--------+---------- 2 | 2001 | bob | 202.0000 3 | 2002 | bob | 707.0000 (2 rows)
حسنًا ، من ناحية ، يجب ألا يرى أمر UPDATE تغييرات المعاملة الثانية. لكن من ناحية أخرى ، يجب ألا تفقد التغييرات التي تم الالتزام بها في المعاملة الثانية.
بمجرد تحرير القفل ، يقوم UPDATE بإعادة قراءة الصف الذي يحاول تحديثه (ولكن هذا فقط). نتيجةً لذلك ، تراكمت Bob9 على أساس مبلغ 900 ₽. ولكن إذا كان بوب لديه 900 دولار ، فلن تكون حساباته في الاختيار على الإطلاق.
لذا ، تحصل المعاملة على بيانات غير صحيحة: تكون بعض الصفوف مرئية في وقت ما ، وبعضها في نقطة أخرى. بدلاً من التحديث المفقود ، نحصل مرة أخرى على شذوذ
القراءة غير المتسقة .
يلاحظ القراء المهتمون أنه مع بعض المساعدة من التطبيق ، يمكنك الحصول على تحديث مفقود حتى على مستوى "قراءة ملتزم". على سبيل المثال:
x := (SELECT amount FROM accounts WHERE id = 1); UPDATE accounts SET amount = x + 100 WHERE id = 1;
لا ينبغي إلقاء اللوم على قاعدة البيانات: فهي تحصل على جملتي SQL ولا تعرف شيئًا عن حقيقة أن قيمة x + 100 ترتبط بطريقة ما بمبلغ الحسابات. تجنب كتابة التعليمات البرمجية بهذه الطريقة.
تكرار القراءة
غياب القراءات غير القابلة للتكرار والشبح
اسم مستوى العزل نفسه يفترض أن القراءة قابلة للتكرار. دعونا التحقق من ذلك ، وفي الوقت نفسه تأكد من عدم وجود قراءات وهمية. للقيام بذلك ، في المعاملة الأولى ، قمنا بإعادة حسابات بوب إلى حالتها السابقة وإنشاء حساب جديد لتشارلي:
=> BEGIN; => UPDATE accounts SET amount = 200.00 WHERE id = 2; => UPDATE accounts SET amount = 800.00 WHERE id = 3; => INSERT INTO accounts VALUES (4, '3001', 'charlie', 100.00); => SELECT * FROM accounts ORDER BY id;
id | number | client | amount ----+--------+---------+-------- 1 | 1001 | alice | 800.00 2 | 2001 | bob | 200.00 3 | 2002 | bob | 800.00 4 | 3001 | charlie | 100.00 (4 rows)
في الجلسة الثانية ، نبدأ المعاملة بمستوى القراءة المكررة عن طريق تحديدها في أمر BEGIN (مستوى المعاملة الأولى غير أساسي).
| => BEGIN ISOLATION LEVEL REPEATABLE READ; | => SELECT * FROM accounts ORDER BY id;
| id | number | client | amount | ----+--------+--------+---------- | 1 | 1001 | alice | 800.00 | 2 | 2001 | bob | 202.0000 | 3 | 2002 | bob | 707.0000 | (3 rows)
الآن تقوم المعاملة الأولى بتنفيذ التغييرات بينما تقوم الثانية بتنفيذ نفس الاستعلام.
=> COMMIT;
| => SELECT * FROM accounts ORDER BY id;
| id | number | client | amount | ----+--------+--------+---------- | 1 | 1001 | alice | 800.00 | 2 | 2001 | bob | 202.0000 | 3 | 2002 | bob | 707.0000 | (3 rows)
| => COMMIT;
لا تزال المعاملة الثانية ترى نفس البيانات تمامًا كما في البداية: لا توجد تغييرات على الصفوف الحالية أو صفوف جديدة مرئية.
في هذا المستوى ، يمكنك تجنب القلق بشأن شيء قد يتغير بين اثنين من المشغلين.
خطأ التسلسل في مقابل التغييرات المفقودة
لقد ناقشنا في وقت سابق أنه عندما يتم تحديث معاملتين الصف نفسه على مستوى "قراءة ملتزم" ، قد تحدث شذوذ في القراءة غير المتسقة. وذلك لأن معاملة الانتظار تعيد قراءة الصف المغلق وبالتالي لا ترى أنه من نفس النقطة الزمنية للصفوف الأخرى.
على مستوى تكرار القراءة ، لا يُسمح بهذا الوضع الشاذ ، لكن في حالة حدوث ذلك ، لا يمكن فعل أي شيء - لذلك تنتهي المعاملة بسبب حدوث خطأ في التسلسل. دعونا التحقق من ذلك من خلال تكرار السيناريو نفسه مع تراكم الفائدة:
=> SELECT * FROM accounts WHERE client = 'bob';
id | number | client | amount ----+--------+--------+-------- 2 | 2001 | bob | 200.00 3 | 2002 | bob | 800.00 (2 rows)
=> BEGIN; => UPDATE accounts SET amount = amount - 100.00 WHERE id = 3;
| => BEGIN ISOLATION LEVEL REPEATABLE READ;<span/> | => UPDATE accounts SET amount = amount * 1.01<span/> | WHERE client IN (<span/> | SELECT client<span/> | FROM accounts<span/> | GROUP BY client<span/> | HAVING sum(amount) >= 1000<span/> | );<span/>
=> COMMIT;
| ERROR: could not serialize access due to concurrent update
| => ROLLBACK;
ظلت البيانات متسقة:
=> SELECT * FROM accounts WHERE client = 'bob';
id | number | client | amount ----+--------+--------+-------- 2 | 2001 | bob | 200.00 3 | 2002 | bob | 700.00 (2 rows)
سيحدث الخطأ نفسه في حالة أي تغيير تنافسي آخر على التوالي ، حتى لو لم يتم تغيير أعمدة اهتمامنا فعليًا.
استنتاج عملي : إذا كان التطبيق الخاص بك يستخدم مستوى عزل القراءة القابلة للتكرار لمعاملات الكتابة ، فيجب أن يكون جاهزًا لتكرار المعاملات التي تم إنهاؤها بخطأ التسلسل. للمعاملات للقراءة فقط ، هذه النتيجة غير ممكنة.
الكتابة غير متناسقة
لذلك ، في PostgreSQL ، على مستوى عزل القراءة المتكررة ، يتم منع جميع الحالات الشاذة الموصوفة في المعيار. ولكن ليس كل الشذوذ بشكل عام. اتضح أن هناك
بالضبط اثنين من الحالات الشاذة التي لا تزال ممكنة. (هذا صحيح ليس فقط بالنسبة لـ PostgreSQL ، ولكن أيضًا للتطبيقات الأخرى لعزل اللقطة.)
أول هذه الحالات الشاذة هي
كتابة غير متناسقة .
دع قاعدة التناسق التالية قائمة:
يُسمح بالمبالغ السلبية على حسابات العملاء إذا ظل المبلغ الإجمالي على جميع حسابات ذلك العميل غير سالب .
تحصل الصفقة الأولى على مبلغ حساب بوب: ₽900.
=> BEGIN ISOLATION LEVEL REPEATABLE READ; => SELECT sum(amount) FROM accounts WHERE client = 'bob';
sum -------- 900.00 (1 row)
المعاملة الثانية تحصل على نفس المبلغ.
| => BEGIN ISOLATION LEVEL REPEATABLE READ; | => SELECT sum(amount) FROM accounts WHERE client = 'bob';
| sum | -------- | 900.00 | (1 row)
تؤمن الصفقة الأولى بحق أنه يمكن تخفيض مبلغ أحد الحسابات بمقدار 600 ₽.
=> UPDATE accounts SET amount = amount - 600.00 WHERE id = 2;
والمعاملة الثانية تأتي إلى نفس النتيجة. ولكنه يقلل من حساب آخر:
| => UPDATE accounts SET amount = amount - 600.00 WHERE id = 3; | => COMMIT;
=> COMMIT; => SELECT * FROM accounts WHERE client = 'bob';
id | number | client | amount ----+--------+--------+--------- 2 | 2001 | bob | -400.00 3 | 2002 | bob | 100.00 (2 rows)
لقد نجحنا في جعل رصيد Bob يتحول إلى اللون الأحمر ، على الرغم من أن كل معاملة تعمل بشكل صحيح.
للقراءة فقط الشذوذ المعاملة
هذا هو الثاني والأخير من الحالات الشاذة الممكنة على مستوى القراءة المتكررة. لإثبات ذلك ، ستحتاج إلى ثلاث معاملات ، اثنان منها سيغيران البيانات ، والثالث سوف يقرأها فقط.
ولكن أولاً دعنا نستعيد حالة حسابات بوب:
=> UPDATE accounts SET amount = 900.00 WHERE id = 2; => SELECT * FROM accounts WHERE client = 'bob';
id | number | client | amount ----+--------+--------+-------- 3 | 2002 | bob | 100.00 2 | 2001 | bob | 900.00 (2 rows)
في المعاملة الأولى ، يتم احتساب الفائدة على المبلغ المتاح على جميع حسابات بوب. تضاف الفائدة إلى أحد حساباته:
=> BEGIN ISOLATION LEVEL REPEATABLE READ;
ثم تقوم معاملة أخرى بسحب الأموال من حساب Bob آخر وتغيير التغييرات:
| => BEGIN ISOLATION LEVEL REPEATABLE READ;
إذا تم الالتزام بالمعاملة الأولى في هذه المرحلة ، فلن تحدث أية حالة شاذة: قد نفترض أنه تم تنفيذ المعاملة الأولى أولاً ثم الثانية (ولكن ليس العكس لأن الصفقة الأولى شهدت حالة الحساب ذات المعرف = 3 قبل ذلك تم تغيير الحساب بواسطة المعاملة الثانية).
لكن تخيل أنه في هذه المرحلة تبدأ المعاملة الثالثة (للقراءة فقط) ، والتي تقرأ حالة بعض الحسابات التي لا تتأثر بالمعاملات الأولى والثانية:
| => BEGIN ISOLATION LEVEL REPEATABLE READ;
| id | number | client | amount | ----+--------+--------+-------- | 1 | 1001 | alice | 800.00 | (1 row)
وفقط بعد اكتمال المعاملة الأولى:
=> COMMIT;
ما الحالة التي يجب أن تراها المعاملة الثالثة الآن؟
| SELECT * FROM accounts WHERE client = 'bob';
بمجرد البدء ، يمكن أن ترى المعاملة الثالثة تغييرات المعاملة الثانية (التي تم الالتزام بها بالفعل) ، ولكن لا يمكن اعتبارها الأولى (التي لم يتم الالتزام بها بعد). من ناحية أخرى ، تأكدنا أعلاه من أنه ينبغي اعتبار المعاملة الثانية قد بدأت بعد الصفقة الأولى. أيا كانت الحالة التي ترى المعاملة الثالثة أنها غير متسقة - فهذه مجرد حالة شاذة في معاملة للقراءة فقط. ولكن على مستوى القراءة المتكررة يُسمح:
| id | number | client | amount | ----+--------+--------+-------- | 2 | 2001 | bob | 900.00 | 3 | 2002 | bob | 0.00 | (2 rows)
| => COMMIT;
تسلسل
مستوى التسلسل يمنع كل الشذوذ ممكن. في الواقع ، تم بناء Serializable على قمة عزل اللقطة. لا تحدث تلك الحالات الشاذة التي لا تحدث مع قراءة مكررة (مثل قراءة متسخة أو غير قابلة للتكرار أو وهمية) على مستوى Serializable أيضًا. ويتم اكتشاف تلك الحالات الشاذة التي تحدث (كتابة غير متناسقة وشذوذ معاملة للقراءة فقط) ، ويتم إحباط المعاملة - يحدث خطأ تسلسل مألوف:
لا يمكن إجراء تسلسل للوصول .
الكتابة غير متناسقة
لتوضيح ذلك ، دعنا نكرر السيناريو بخطأ شاذ غير متناسق:
=> BEGIN ISOLATION LEVEL SERIALIZABLE; => SELECT sum(amount) FROM accounts WHERE client = 'bob';
sum ---------- 910.0000 (1 row)
| => BEGIN ISOLATION LEVEL SERIALIZABLE; | => SELECT sum(amount) FROM accounts WHERE client = 'bob';
| sum | ---------- | 910.0000 | (1 row)
=> UPDATE accounts SET amount = amount - 600.00 WHERE id = 2;
| => UPDATE accounts SET amount = amount - 600.00 WHERE id = 3; | => COMMIT;
=> COMMIT;
ERROR: could not serialize access due to read/write dependencies among transactions DETAIL: Reason code: Canceled on identification as a pivot, during commit attempt. HINT: The transaction might succeed if retried.
تمامًا كما في مستوى تكرار القراءة ، يجب على التطبيق الذي يستخدم مستوى العزل Serializable تكرار المعاملات التي تم إنهاؤها مع وجود خطأ في التسلسل ، حيث أن رسالة الخطأ تطالبنا.
نكتسب بساطة البرمجة ، لكن ثمن ذلك هو الإنهاء القسري لبعض الكسور من المعاملات وضرورة تكرارها. السؤال ، بالطبع ، هو حجم هذا الكسر. إذا كانت تلك المعاملات التي تم إنهاؤها والتي تتداخل بشكل غير متعارض مع المعاملات الأخرى فقط ، فستكون لطيفة. ولكن مثل هذا التنفيذ سيكون حتما كثيف الاستخدام للموارد وغير فعال لأنه سيتعين عليك تتبع العمليات في كل صف.
في الواقع ، فإن تطبيق PostgreSQL هو أمر يتيح سلبيات خاطئة: بعض المعاملات العادية تمامًا التي هي "سيئ الحظ" ستُجهض أيضًا. كما سنرى لاحقًا ، يعتمد هذا على العديد من العوامل ، مثل توفر الفهارس المناسبة أو مقدار ذاكرة الوصول العشوائي المتاحة. بالإضافة إلى ذلك ، هناك بعض قيود التنفيذ (شديدة الشدة) ، على سبيل المثال ، لن تعمل الاستعلامات على مستوى Serializable على النسخ المتماثلة ولن تستخدم خطط تنفيذ موازية. على الرغم من استمرار العمل على تحسين التنفيذ ، فإن القيود الحالية تجعل هذا المستوى من العزلة أقل جاذبية.
ستظهر الخطط الموازية في وقت مبكر كما في PostgreSQL 12 ( patch ). يمكن أن تبدأ الاستعلامات المتعلقة بالنسخ المتماثلة في العمل في PostgreSQL 13 ( تصحيح آخر ).
للقراءة فقط الشذوذ المعاملة
بالنسبة لمعاملة للقراءة فقط لا ينتج عنها شذوذ ولا تعاني منها ، تقدم PostgreSQL تقنية مثيرة للاهتمام: يمكن تأمين هذه المعاملة حتى يتم تنفيذها بشكل آمن. هذه هي الحالة الوحيدة عندما يمكن تأمين مشغل SELECT عن طريق تحديثات الصف. هذا هو ما يبدو هذا:
=> UPDATE accounts SET amount = 900.00 WHERE id = 2; => UPDATE accounts SET amount = 100.00 WHERE id = 3; => SELECT * FROM accounts WHERE client = 'bob' ORDER BY id;
id | number | client | amount ----+--------+--------+-------- 2 | 2001 | bob | 900.00 3 | 2002 | bob | 100.00 (2 rows)
=> BEGIN ISOLATION LEVEL SERIALIZABLE;
| => BEGIN ISOLATION LEVEL SERIALIZABLE;
تم الإعلان عن المعاملة الثالثة بشكل صريح "قراءة فقط" و "محتمل":
| => BEGIN ISOLATION LEVEL SERIALIZABLE READ ONLY DEFERRABLE;
عند محاولة تنفيذ الاستعلام ، تكون المعاملة مؤمنة لأنه بخلاف ذلك سيتسبب في حدوث خلل.
=> COMMIT;
وفقط بعد الالتزام بالمعاملة الأولى ، تستمر الصفقة الثالثة في التنفيذ:
| id | number | client | amount | ----+--------+--------+-------- | 1 | 1001 | alice | 800.00 | (1 row)
| => SELECT * FROM accounts WHERE client = 'bob';
| id | number | client | amount | ----+--------+--------+---------- | 2 | 2001 | bob | 910.0000 | 3 | 2002 | bob | 0.00 | (2 rows)
| => COMMIT;
ملاحظة مهمة أخرى: إذا تم استخدام عزل Serializable ، يجب أن تستخدم جميع المعاملات في التطبيق هذا المستوى. لا يمكنك خلط معاملات القراءة-المرتكبة (أو القابلة للتكرار) مع Serializable. وهذا يعني أنه يمكنك المزج ، ولكن بعد ذلك سوف تتصرف Serializable مثل Repeatable Read دون أي تحذيرات. سنناقش لماذا يحدث هذا في وقت لاحق ، عندما نتحدث عن التنفيذ.
لذلك إذا قررت استخدام Serializble ، فمن الأفضل تعيين المستوى الافتراضي على المستوى العالمي (على الرغم من أن هذا بالطبع لن يمنعك من تحديد مستوى غير صحيح بشكل صريح):
ALTER SYSTEM SET default_transaction_isolation = 'serializable';
يمكنك العثور على عرض أكثر صرامة للقضايا المتعلقة بالمعاملات والاتساق والشذوذ في دورة الكتاب والمحاضرات التي أعدها بوريس نوفيكوف "أساسيات تقنيات قواعد البيانات" (متوفر في Russion فقط).
ما مستوى العزلة للاستخدام؟
يتم استخدام مستوى عزل القراءة الملتزم افتراضيًا في PostgreSQL ، ومن المحتمل أن يتم استخدام هذا المستوى في الغالبية العظمى من التطبيقات. هذا الإعداد الافتراضي مناسب لأنه في هذا المستوى لا يمكن إجهاض المعاملة إلا في حالة الفشل ، ولكن ليس كوسيلة لمنع التناقض. بمعنى آخر ، لا يمكن أن يحدث خطأ في التسلسل.
الجانب الآخر للعملة هو عدد كبير من الحالات الشاذة التي تمت مناقشتها بالتفصيل أعلاه. يجب على مهندس البرمجيات دائمًا أن يضعها في الاعتبار وأن يكتب الكود حتى لا يسمح له بالظهور. إذا لم تتمكن من رمز الإجراءات اللازمة في عبارة SQL واحدة ، فيجب عليك اللجوء إلى القفل الصريح. الأكثر إثارة للقلق هو أنه من الصعب اختبار الشفرة عن الأخطاء المرتبطة بالحصول على بيانات غير متسقة ، ويمكن أن تحدث الأخطاء نفسها بطرق غير متوقعة وغير قابلة للتكرار وبالتالي يصعب إصلاحها.
يعمل مستوى عزل القراءة المتكررة على التخلص من بعض مشكلات عدم الاتساق ، ولكن للأسف ، ليس كل ذلك. لذلك ، يجب ألا تتذكر فقط الحالات الشاذة المتبقية ، ولكن يجب عليك أيضًا تعديل التطبيق بحيث يتعامل مع أخطاء التسلسل بشكل صحيح. هو بالتأكيد غير مريح. ولكن بالنسبة للمعاملات للقراءة فقط ، يكمل هذا المستوى تمامًا Read Committed وهو مناسب جدًا ، على سبيل المثال ، لإنشاء تقارير تستخدم استعلامات SQL متعددة.
أخيرًا ، يتيح لك المستوى التسلسلي عدم القلق بشأن عدم الاتساق على الإطلاق ، مما يسهل الترميز إلى حد كبير. الشيء الوحيد المطلوب من التطبيق هو أن تكون قادرًا على تكرار أي معاملة عند الحصول على خطأ في التسلسل. ولكن يمكن لكسر المعاملات المجهولة والمصروفات الإضافية الإضافية وعدم القدرة على موازنة الاستعلامات أن يقلل بشكل كبير من إنتاجية النظام. لاحظ أيضًا أن مستوى Serializable غير قابل للتطبيق على النسخ المتماثلة وأنه لا يمكن خلطه بمستويات العزل الأخرى.
اقرأ على .