كيفية إدارة التزامن في نماذج جانغو

للحصول على تجربة قراءة أفضل ، راجع هذه المقالة على موقع الويب الخاص بي.

لقد ولت أيام أنظمة سطح المكتب التي تخدم مستخدمين فرديين - تطبيقات الويب هذه الأيام تخدم ملايين المستخدمين في نفس الوقت. مع العديد من المستخدمين تأتي مجموعة واسعة من المشاكل الجديدة - مشاكل التزامن.

في هذه المقالة سأقدم طريقتين لإدارة التزامن في نماذج جانغو.

تصوير دينيس نيفوزهاي

المشكلة

لإظهار مشكلات التوافق الشائعة ، سنعمل على نموذج حساب مصرفي:

حساب فئة (نماذج. نموذج):
    المعرف = models.AutoField (
        primary_key = صحيح،
    )
    المستخدم = الموديلات.
        المستعمل،
    )
    الرصيد = الموديلات.
        الافتراضي = 0،
    )

للبدء ، سنطبق طرق إيداع وسحب ساذجة لمثيل حساب:

إيداع def (النفس ، المبلغ):
    التوازن الذاتي + = المبلغ
    self.save ()
سحب def (النفس ، المبلغ):
    إذا كان المبلغ> التوازن الذاتي:
        رفع الأخطاء.
    التوازن الذاتي - = المبلغ
    self.save ()

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

  1. المستخدم أ يجلب الحساب - الرصيد هو 100 دولار.
  2. المستخدم ب يجلب الحساب - الرصيد هو 100 دولار.
  3. يسحب المستخدم ب 30 دولارًا - يتم تحديث الرصيد إلى 100 دولار - 30 دولار = 70 دولارًا.
  4. ودائع المستخدم أ 50 دولار - يتم تحديث الرصيد إلى 100 دولار + 50 دولار = 150 دولار.

ماذا حدث هنا؟

طلب المستخدم ب سحب 30 دولارًا وأودع المستخدم أ 50 دولارًا - نتوقع أن يكون الرصيد 120 دولارًا ، ولكن انتهى بنا الأمر إلى 150 دولارًا.

لماذا حصل هذا؟

في الخطوة 4 ، عندما قام المستخدم A بتحديث الرصيد ، كان المبلغ الذي كان قد خزنه في الذاكرة لا يزال قديمًا (كان المستخدم B قد سحب بالفعل 30 دولارًا).

لمنع حدوث هذا الموقف ، نحتاج إلى التأكد من عدم تغيير المورد الذي نعمل عليه أثناء العمل عليه.

نهج متشائم

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

للحصول على قفل على مورد ، نستخدم قفل قاعدة بيانات لعدة أسباب:

  1. قواعد البيانات (العلائقية) جيدة جدًا في إدارة الأقفال والحفاظ على الاتساق.
  2. قاعدة البيانات هي أدنى مستوى يتم فيه الوصول إلى البيانات - سيؤدي الحصول على القفل في أدنى مستوى إلى حماية البيانات من العمليات الأخرى التي تعدل البيانات أيضًا. على سبيل المثال ، قم بتحديث التحديثات مباشرة في قاعدة البيانات ، وظائف cron ، مهام التنظيف ، إلخ.
  3. يمكن تشغيل تطبيق Django على عمليات متعددة (مثل العمال). تتطلب المحافظة على الأقفال على مستوى التطبيق الكثير من العمل (غير الضروري).

لقفل كائن في جانغو نستخدم select_for_update.

دعنا نستخدم النهج المتشائم لتنفيذ إيداع آمن وسحب:

classmethod
إيداع def (cls ، id ، المبلغ):
   مع المعاملة. الذرية ():
       الحساب = (
           cls.objects
           .select_for_update ()
           . احصل على (ID = معرف)
       )
      
       account.balance + = المبلغ
       account.save ()
    عودة الحساب
classmethod
سحب def (cls ، id ، المبلغ):
   مع المعاملة. الذرية ():
       الحساب = (
           cls.objects
           .select_for_update ()
           . احصل على (ID = معرف)
       )
      
       إذا account.balance <المبلغ:
           رفع الاخطاء.
       account.balance - = المبلغ
       account.save ()
  
   عودة الحساب

ماذا لدينا هنا:

  1. نستخدم select_for_update على queryset الخاصة بنا لإخبار قاعدة البيانات بقفل الكائن حتى تتم المعاملة.
  2. يتطلب تأمين صف في قاعدة البيانات معاملة قاعدة بيانات - نستخدم المعاملة الخاصة بديكور Django.atomic () لنطاق المعاملة.
  3. نستخدم نظام الفصل الدراسي بدلاً من طريقة المثيل - للحصول على القفل الذي نحتاجه لإخبار قاعدة البيانات بقفله. لتحقيق ذلك ، نحن بحاجة إلى أن نكون الأشخاص الذين يقومون بجلب الكائن من قاعدة البيانات. عند التشغيل على الذات ، يتم إحضار الكائن بالفعل وليس لدينا أي ضمانات بأنه تم إغلاقه.
  4. يتم تنفيذ جميع العمليات على الحساب ضمن معاملة قاعدة البيانات.

دعونا نرى كيف يتم منع السيناريو السابق من خلال تطبيقنا الجديد:

  1. يطلب المستخدم أ سحب 30 $:
    - يكتسب المستخدم أ قفل الحساب.
    - الرصيد هو 100 دولار.
  2. يطلب المستخدم ب إيداع 50 $:
    - فشل محاولة الحصول على تأمين على الحساب (مؤمن من قبل المستخدم أ).
    - ينتظر المستخدم B قفل القفل.
  3. سحب المستخدم 30 دولارًا:
    - الرصيد هو 70 دولار.
    - يتم تحرير قفل المستخدم A على الحساب.
  4. يحصل المستخدم ب على قفل على الحساب.
    - الرصيد هو 70 دولار.
    - الرصيد الجديد هو 70 دولار + 50 دولار = 120 دولار.
  5. يتم تحرير قفل المستخدم B على الحساب ، والتوازن هو 120 دولار.

علة منعت!

ما تحتاج لمعرفته حول select_for_update:

  • في السيناريو الخاص بنا ، انتظر المستخدم B أن يقوم المستخدم A بإطلاق القفل. بدلاً من الانتظار ، يمكننا إخبار Django بعدم انتظار القفل لإصدار ورفع DatabaseError بدلاً من ذلك. للقيام بذلك ، يمكننا ضبط وسيطة nowait الخاصة بـ select_for_update على True ، ... select_for_update (nowait = True).
  • تحديد الكائنات ذات الصلة مؤمنة أيضًا - عند استخدام select_for_update مع select_related ، يتم أيضًا تأمين الكائنات ذات الصلة.
    على سبيل المثال ، إذا أردنا تحديد_ربط المستخدم مع الحساب ، فسيتم تأمين كل من المستخدم والحساب. إذا حاول شخص ما أثناء الإيداع ، على سبيل المثال ، تحديث الاسم الأول ، فسيفشل هذا التحديث بسبب قفل كائن المستخدم.
    إذا كنت تستخدم PostgreSQL أو Oracle ، فقد لا تكون هذه مشكلة قريبًا بفضل ميزة جديدة في إصدار Django 2.0 القادم. في هذا الإصدار ، يحتوي select_for_update على خيار "من" لتوضيح أي الجداول في الاستعلام تقفل بشكل صريح.

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

نهج متفائل

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

كيف يمكننا تنفيذ مثل هذا الشيء مع جانغو؟

أولاً ، نضيف عمودًا لتتبع التغييرات التي تم إجراؤها على الكائن:

الإصدار = models.IntegerField (
    الافتراضي = 0،
)

ثم ، عندما نقوم بتحديث كائن ، نتأكد من أن الإصدار لم يتغير:

إيداع def (النفس ، معرف ، المبلغ):
   محدث = Account.objects.filter (
       معرف = self.id،
       نسخة = self.version،
   ).تحديث(
       الرصيد = الرصيد + المبلغ ،
       الإصدار = self.version + 1 ،
   )
   عودة تحديث> 0
سحب def (النفس ، معرف ، المبلغ):
   إذا التوازن الذاتي <المبلغ:
       رفع الاخطاء.
  
   محدث = Account.objects.filter (
       معرف = self.id،
       نسخة = self.version،
   ).تحديث(
       الرصيد = الرصيد - المبلغ ،
       الإصدار = self.version + 1 ،
   )
  
   عودة تحديث> 0

دعنا نقسمها:

  1. نحن نعمل مباشرة على سبيل المثال (لا classmethod).
  2. نعتمد على حقيقة أن الإصدار يزداد كلما تم تحديث الكائن.
  3. نقوم بتحديث فقط إذا لم يتغير الإصدار:
    - إذا لم يتم تعديل الكائن نظرًا لأننا جلبناه من يتم تحديث الكائن.
    - إذا تم تعديله من الاستعلام ، فسوف يُرجع صفر سجلات ولن يتم تحديث الكائن.
  4. ترجع Django عدد الصفوف المحدّثة. إذا كان "المحدّث" يساوي الصفر ، فهذا يعني أن شخصًا آخر غيّر الكائن من وقت إحضاره.

كيف يعمل قفل التفاؤل في السيناريو:

  1. المستخدم أ يجلب الحساب - الرصيد هو 100 دولار ، الإصدار هو 0.
  2. المستخدم ب إحضار الحساب - الرصيد هو 100 دولار ، الإصدار هو 0.
  3. يطلب المستخدم ب سحب 30 $:
    - يتم تحديث الرصيد إلى 100 دولار - 30 دولار = 70 دولار.
    - يتم زيادة الإصدار إلى 1.
  4. يطلب المستخدم أ إيداع 50 $:
    - الرصيد المحسوب هو 100 دولار + 50 دولار = 150 دولار.
    - الحساب غير موجود مع الإصدار 0 -> يتم تحديث أي شيء.

ما تحتاج إلى معرفته عن النهج المتفائل:

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

أي واحدة علي أن أستخدم؟

مثل أي سؤال عظيم ، فإن الجواب "يعتمد":

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