MVCC-1. العزلة

مرحبا يا هبر! مع هذه المقالة ، أبدأ سلسلة من الحلقات (أو حلقة من السلسلة؟ بشكل عام ، فكرة عظيمة) حول الهيكل الداخلي لبرنامج PostgreSQL.

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

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

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

الأمور التي ستتم مناقشتها لا تتغير كثيرًا من إصدار إلى آخر ، لكنني سأستخدم PostgreSQL "الفانيليا" الحالي ، الحادي عشر.

تكرس الدورة الأولى للقضايا المتعلقة بالعزلة والتعددية ، وخطتها هي كما يلي:

  1. العزلة ، كما يفهمها المعيار و PostgreSQL (هذه المقالة) ؛
  2. الطبقات والملفات والصفحات - ما يحدث على المستوى المادي ؛
  3. إصدارات الصف والمعاملات الافتراضية والمتداخلة ؛
  4. لقطات البيانات وضوح إصدارات الصف ، أفق الحدث ؛
  5. في الصفحة التنظيف والتحديثات الساخنة .
  6. التنظيف العادي (فراغ) ؛
  7. التنظيف التلقائي (autov Vacuum) ؛
  8. تجاوز عداد المعاملات وتجميدها .

حسنا ، دعنا نذهب.

ما هو العزل ولماذا هو مهم؟


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

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

ولكن ما هي البيانات "الصحيحة"؟ من المعروف أنه على مستوى قاعدة البيانات ، يمكنك إنشاء قيود تكامل (مثل NOT NULL أو UNIQUE). إذا كانت البيانات تفي دائمًا بقيود التكامل (وهذا هو السبب في أن قواعد بيانات إدارة قواعد البيانات تضمن ذلك) ، فهي جزء لا يتجزأ.

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

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

من الآن فصاعدا ، سوف نسمي الصحيح مصطلح الاتساق.

لنفترض ، مع ذلك ، أن التطبيق ينفذ التسلسل الصحيح للبيانات فقط. ما هو دور DBMS إذن ، إذا كان التطبيق صحيحًا؟

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

التمرين الجيد هو تنفيذ القاعدة الموضحة أعلاه على مستوى قيود النزاهة. هل انت ضعيف ©

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

اتضح أنه مثير للاهتمام: مع العلم أن العمليات تشكل معاملة ، يساعد نظام إدارة قواعد البيانات (DBMS) في الحفاظ على الاتساق من خلال ضمان ذرية المعاملات ، مع عدم معرفة أي شيء عن قواعد التناسق المحددة.

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

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

تسمى المواقف التي لا تعمل فيها المعاملات الصحيحة معًا بشكل صحيح ، الحالات الشاذة للتنفيذ المتزامن.

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

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

بالطبع ، من المستحيل رفض التنفيذ المتزامن: وإلا ، ما نوع الأداء الذي يمكن مناقشته؟ لكن لا يمكنك العمل مع بيانات غير صحيحة.

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

لذلك ، نأتي إلى التعريف:

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

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

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

مستويات عزل SQL والشذوذ


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

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

فقدت التحديث


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

على سبيل المثال ، ستعمل معاملتان على زيادة المبلغ على نفس الحساب بمقدار 100 ₽. تقوم المعاملة الأولى بقراءة القيمة الحالية (1000 ₽) ، ثم تقوم المعاملة الثانية بقراءة نفس القيمة. المعاملة الأولى تزيد المبلغ (اتضح 1100 ₽) وتكتب هذه القيمة. المعاملة الثانية تفعل الشيء نفسه - تحصل على نفس 1100 ₽ وتكتب لهم. نتيجة لذلك ، فقد العميل 100 ₽.

لا يُسمح بالتحديثات المفقودة بموجب المعيار في أي مستوى عزل.

القراءة القذرة والقراءة غير ملتزم بها


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

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

القراءة القذرة مسموح بها بواسطة المعيار على مستوى القراءة غير الملتزم بها.

غير كرر قراءة وقراءة ملتزم


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

على سبيل المثال ، دع قاعدة التناسق تحظر المبالغ السلبية في حسابات العملاء . ستعمل الصفقة الأولى على تقليل المبلغ في الحساب بمقدار 100 ₽. إنها تتحقق من القيمة الحالية ، وتحصل على 1000 درجة مئوية وتقرر أن التخفيض ممكن. في هذا الوقت ، تعمل المعاملة الثانية على تقليل المبلغ في الحساب إلى الصفر وتسجيل التغييرات. إذا قامت الآن الصفقة الأولى بإعادة التحقق من المبلغ ، فستتلقى 0 ₽ (لكنها قررت بالفعل خفض القيمة ، والحساب "يذهب إلى ناقص").

القراءة غير المتكررة مسموح بها بواسطة المعيار في مستويات القراءة غير الملتزم بها والقراءة. ولكن القراءة القذرة قراءة ملتزم لا يسمح.

الوهمية القراءة والقراءة المتكررة


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

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

يُسمح بالقراءة الوهمية وفقًا لمعايير القراءة في مستويات القراءة غير الملتزم بها والقراءة الملتزمة والقابلة للتكرار. ولكن على مستوى القراءة المتكررة ، لا يُسمح بالقراءة غير المتكررة.

عدم وجود حالات شذوذ وسلسلة


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

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

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

لوحة ملخص


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

لماذا بالضبط هذه الحالات الشاذة؟


لماذا لا يوجد سوى عدد قليل من الحالات الشاذة المحتملة في المعيار المدرجة ، ولماذا هذه؟

على ما يبدو ، لا أحد يبدو أن يعرف هذا بالتأكيد. لكن الممارسة هنا تجاوزت بالتأكيد النظرية ، لذلك من الممكن أننا لم نفكر في حالات شاذة أخرى (خطاب حول معيار SQL: 92).

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

يبدو لي أن الفرق بين مستويات عزل المعيار يتم تفسيره بدقة بعدد الأقفال اللازمة.

إذا كانت إحدى المعاملات تمنع الصفوف المعدلة من التغيير ، ولكن ليس من القراءة ، فسنحصل على مستوى القراءة غير الملتزم بها: لا يُسمح بالتغييرات المفقودة ، ولكن يمكن قراءة البيانات غير الملتزم بها.

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

إذا كانت المعاملة تمنع كل من الخطوط القابلة للقراءة والقابلة للتغيير من القراءة والتغيير ، فسنحصل على مستوى القراءة المتكررة: ستؤدي القراءة المتكررة للخط إلى الحصول على نفس القيمة.

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

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

مستويات عزل بوستجرس


بمرور الوقت ، حل Snapshot Isolation محل بروتوكولات إدارة المعاملات المحظورة . فكرته هي أن كل معاملة تعمل مع لقطة متسقة للبيانات في وقت معين ، حيث فقط تلك التغييرات التي تم تسجيلها قبل إنشاء اللقطة.

هذه العزلة لا تسمح تلقائيا القراءة القذرة. بشكل رسمي ، في PostgreSQL ، يمكنك تحديد مستوى "قراءة غير ملتزم بها" ، لكنه سيعمل تمامًا مثل "قراءة ملتزم". لذلك ، لن نتحدث عن قراءة المستوى غير الملتزم به.

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

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

كيف يتم تطبيق الإصدارات المتعددة "تحت الغطاء" ، سنتحدث في المقالات التالية ، والآن سننظر بالتفصيل في كل مستوى من المستويات الثلاثة من خلال عيون المستخدم (كما تعلمون ، يتم إخفاء الأكثر إثارة للاهتمام وراء "الحالات الشاذة الأخرى"). للقيام بذلك ، قم بإنشاء جدول حسابات. يمتلك كل من Alice and 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 ----- | UPDATE accounts SET amount = amount - 200 WHERE id = 1; | COMMIT; ----- UPDATE accounts SET amount = amount - 1000 WHERE id = 1; END IF; 

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

كيف تكتب الكود بشكل صحيح؟ الفرص ، كقاعدة عامة ، تتلخص في الآتي:

  • لا تكتب الرمز.
    هذه ليست مزحة. على سبيل المثال ، في هذه الحالة ، يتحول الاختيار بسهولة إلى قيد النزاهة:
    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; 

وبالتالي ، تلقت المعاملة الثانية ما مجموعه 1100 ₽ ، أي بيانات غير صحيحة. هذا هو الشذوذ في القراءة غير متناسقة .

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

  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) 

أؤكد أن مثل هذا التأثير ممكن فقط على مستوى عزل Read Committed ، وفقط مع فئة تباين VOLATILE. المشكلة هي أن هذا المستوى من العزلة وهذه الفئة من التقلبات يتم استخدامها بشكل افتراضي ، لذلك يجب أن أعترف - أشعل النار يكمن بشكل جيد للغاية. لا تخطو!

قراءة غير متسقة في مقابل التغييرات المفقودة


يمكن الحصول على قراءة غير متناسقة في إطار مشغل واحد - بطريقة غير متوقعة إلى حد ما - أثناء التحديث.

دعونا نرى ما يحدث عندما تحاول تغيير نفس الصف مع اثنين من المعاملات. بوب لديه الآن 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 بإعادة قراءة السطر الذي يحاول تحديثه (ولكن واحد فقط!). والنتيجة هي أن بوب تراكم 9 ₽ ، على أساس مبلغ 900 ₽. ولكن إذا كان بوب لديه 900 ₽ ، فلن يتم تضمين حساباته في العينة على الإطلاق.

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

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

  x := (SELECT amount FROM accounts WHERE id = 1); UPDATE accounts SET amount = x + 100 WHERE id = 1; 

لا ينبغي إلقاء اللوم على قاعدة البيانات: فهي تتلقى جملتي SQL ولا تعرف أي شيء ترتبط قيمة x + 100 بطريقة ما بـ accounts.amount. لا تكتب الكود بهذه الطريقة.

تكرار القراءة


قلة التكرار والقراءات الوهمية


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

 => 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; | => UPDATE accounts SET amount = amount * 1.01 | WHERE client IN ( | SELECT client | FROM accounts | GROUP BY client | HAVING sum(amount) >= 1000 | ); 

 => 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; -- 1 => UPDATE accounts SET amount = amount + ( SELECT sum(amount) FROM accounts WHERE client = 'bob' ) * 0.01 WHERE id = 2; 

ثم تقوم معاملة أخرى بسحب الأموال من حساب آخر لبوب والتقاط تغييراته:

 | => BEGIN ISOLATION LEVEL REPEATABLE READ; -- 2 | => UPDATE accounts SET amount = amount - 100.00 WHERE id = 3; | => COMMIT; 

إذا تم الالتزام بالمعاملة الأولى في هذه اللحظة ، فلن يكون هناك أي حالة شاذة: قد نفترض أن المعاملة الأولى قد اكتملت أولاً ، ثم الثانية (ولكن ليس العكس ، لأن المعاملة الأولى شهدت حالة معرّف الحساب = 3 قبل أن يكون هذا الحساب) تغيرت من الصفقة الثانية).

لكن لنفترض أنه في هذه اللحظة تبدأ المعاملة الثالثة (للقراءة فقط) ، والتي تقرأ حالة بعض الحسابات التي لا تتأثر بالمعاملات الأولى والثانية:

 | => BEGIN ISOLATION LEVEL REPEATABLE READ; -- 3 | => SELECT * FROM accounts WHERE client = 'alice'; 
 | 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 ( تصحيح ). يمكن أن تكسب الاستعلامات على النسخ المتماثلة في 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; -- 1 => UPDATE accounts SET amount = amount + ( SELECT sum(amount) FROM accounts WHERE client = 'bob' ) * 0.01 WHERE id = 2; 

 | => BEGIN ISOLATION LEVEL SERIALIZABLE; -- 2 | => UPDATE accounts SET amount = amount - 100.00 WHERE id = 3; | => COMMIT; 

يتم الإعلان عن المعاملة الثالثة صراحة فقط من قبل القارئ (قراءة فقط) وتأجيلها (محتمل):

 | => BEGIN ISOLATION LEVEL SERIALIZABLE READ ONLY DEFERRABLE; -- 3 | => SELECT * FROM accounts WHERE client = 'alice'; 

عندما تحاول تنفيذ طلب ما ، يتم حظر المعاملة ، وإلا فسيؤدي تنفيذها إلى حدوث خلل.

 => 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 كقراءة متكررة دون سابق إنذار. , , .

Serializble — ( , , ):

  ALTER SYSTEM SET default_transaction_isolation = 'serializable'; 

, , , « ».

?


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

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

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

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

أن تستمر .

Source: https://habr.com/ru/post/ar442804/


All Articles