أقفال في بوستجرس: 3. أقفال كائنات أخرى

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

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

المآزق


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

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



بالطبع ، الجمود ممكن ليس فقط للمعاملات ، ولكن أيضًا لأي عدد أكبر.

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

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

إذا لم يكشف الشيك (الذي يتكون في بناء رسم بياني للتوقعات والبحث عن ملامح فيه) عن طريق مسدود ، فإن العملية تستمر في النوم - الآن بالفعل إلى النهاية المريرة.

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

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

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

مثال حالة توقف تام


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

=> BEGIN; => UPDATE accounts SET amount = amount - 100.00 WHERE acc_no = 1; 
 UPDATE 1 

في الوقت نفسه ، تعتزم الصفقة الثانية نقل 10 روبل من الحساب الثاني إلى الأول. انها تبدأ من خلال الحد من العد الثاني:

 | => BEGIN; | => UPDATE accounts SET amount = amount - 10.00 WHERE acc_no = 2; 
 | UPDATE 1 

الآن تحاول المعاملة الأولى زيادة الحساب الثاني ، لكنها ترى أن الصف مغلق.

 => UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 2; 

ثم تحاول المعاملة الثانية زيادة الحساب الأول ، ولكن يتم حظره أيضًا.

 | => UPDATE accounts SET amount = amount + 10.00 WHERE acc_no = 1; 

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

 ERROR: deadlock detected DETAIL: Process 16477 waits for ShareLock on transaction 530695; blocked by process 16513. Process 16513 waits for ShareLock on transaction 530694; blocked by process 16477. HINT: See server log for query details. CONTEXT: while updating tuple (0,2) in relation "accounts" 

الآن يمكن أن تستمر الصفقة الثانية.

 | UPDATE 1 
 | => ROLLBACK; 

 => ROLLBACK; 

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

حالة توقف تام لاثنين من أوامر التحديث


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

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

 => CREATE INDEX ON accounts(amount DESC); 

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

 => CREATE FUNCTION inc_slow(n numeric) RETURNS numeric AS $$ SELECT pg_sleep(1); SELECT n + 100.00; $$ LANGUAGE SQL; 

نحن بحاجة أيضا إلى تمديد pgrowlocks.

 => CREATE EXTENSION pgrowlocks; 

سيقوم الأمر UPDATE الأول بتحديث الجدول بأكمله. خطة التنفيذ واضحة - فحص تسلسلي:

 | => EXPLAIN (costs off) | UPDATE accounts SET amount = inc_slow(amount); 
 | QUERY PLAN | ---------------------------- | Update on accounts | -> Seq Scan on accounts | (2 rows) 

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

 | => UPDATE accounts SET amount = inc_slow(amount); 

وفي الوقت نفسه ، في جلسة أخرى ، سنحظر استخدام المسح التسلسلي:

 || => SET enable_seqscan = off; 

في هذه الحالة ، يقرر المجدول استخدام تفحص الفهرس لبيان UPDATE التالي:

 || => EXPLAIN (costs off) || UPDATE accounts SET amount = inc_slow(amount) WHERE amount > 100.00; 
 || QUERY PLAN || -------------------------------------------------------- || Update on accounts || -> Index Scan using accounts_amount_idx on accounts || Index Cond: (amount > 100.00) || (3 rows) 

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

نطلق التحديث القادم.

 || => UPDATE accounts SET amount = inc_slow(amount) WHERE amount > 100.00; 

توضح النظرة السريعة على الصفحة المجدولة أن المشغل الأول قد تمكن بالفعل من تحديث الصف الأول (0،1) والثاني - الأخير (0،3):

 => SELECT * FROM pgrowlocks('accounts') \gx 
 -[ RECORD 1 ]----------------- locked_row | (0,1) locker | 530699 <-  multi | f xids | {530699} modes | {"No Key Update"} pids | {16513} -[ RECORD 2 ]----------------- locked_row | (0,3) locker | 530700 <-  multi | f xids | {530700} modes | {"No Key Update"} pids | {16549} 

يمر ثانية أخرى. قام المشغل الأول بتحديث السطر الثاني ، والثاني يود القيام بذلك ، لكن لا يمكنه ذلك.

 => SELECT * FROM pgrowlocks('accounts') \gx 
 -[ RECORD 1 ]----------------- locked_row | (0,1) locker | 530699 <-  multi | f xids | {530699} modes | {"No Key Update"} pids | {16513} -[ RECORD 2 ]----------------- locked_row | (0,2) locker | 530699 <-    multi | f xids | {530699} modes | {"No Key Update"} pids | {16513} -[ RECORD 3 ]----------------- locked_row | (0,3) locker | 530700 <-  multi | f xids | {530700} modes | {"No Key Update"} pids | {16549} 

الآن يود البيان الأول تحديث الصف الأخير من الجدول ، لكنه مغلق بالفعل بالصف الثاني. هنا هو الجمود.

تم إحباط إحدى المعاملات:

 || ERROR: deadlock detected || DETAIL: Process 16549 waits for ShareLock on transaction 530699; blocked by process 16513. || Process 16513 waits for ShareLock on transaction 530700; blocked by process 16549. || HINT: See server log for query details. || CONTEXT: while updating tuple (0,2) in relation "accounts" 

والآخر يكمل التنفيذ:

 | UPDATE 3 

يمكن العثور على تفاصيل مثيرة للاهتمام حول الكشف عن حالة توقف تام ومنعها في مدير قفل README .

هذا كل شيء عن حالة توقف تام ، ونحن ننتقل إلى أقفال الكائنات المتبقية.



أقفال غير علاقة


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

دعنا ننظر إلى مثال بسيط. نبدأ المعاملة وننشئ جدولًا فيها:

 => BEGIN; => CREATE TABLE example(n integer); 

الآن لنرى ما نوع تأمين الكائنات الذي ظهر في pg_locks:

 => SELECT database, (SELECT datname FROM pg_database WHERE oid = l.database) AS dbname, classid, (SELECT relname FROM pg_class WHERE oid = l.classid) AS classname, objid, mode, granted FROM pg_locks l WHERE l.locktype = 'object' AND l.pid = pg_backend_pid(); 
  database | dbname | classid | classname | objid | mode | granted ----------+--------+---------+--------------+-------+-----------------+--------- 0 | | 1260 | pg_authid | 16384 | AccessShareLock | t 16386 | test | 2615 | pg_namespace | 2200 | AccessShareLock | t (2 rows) 

لفهم ما تم حظره بالضبط هنا ، تحتاج إلى إلقاء نظرة على ثلاثة مجالات: قاعدة البيانات ، classid و objid. لنبدأ بالسطر الأول.

قاعدة البيانات هي OID لقاعدة البيانات التي ينتمي إليها المورد المؤمن. في حالتنا ، يوجد صفر في هذا العمود. هذا يعني أننا نتعامل مع كائن عالمي لا ينتمي إلى أي قاعدة معينة.

يحتوي Classid على OID الخاص بـ pg_class ، والذي يتوافق مع اسم جدول كتالوج النظام ، والذي يحدد نوع المورد. في حالتنا ، pg_authid ، أي أن الدور هو المورد (المستخدم).

يحتوي Objid على OID من جدول كتالوج النظام الذي أشار إليه classid لنا.

 => SELECT rolname FROM pg_authid WHERE oid = 16384; 
  rolname --------- student (1 row) 

وبالتالي ، يتم حظر دور الطالب ، الذي نعمل منه.

الآن دعونا نتعامل مع السطر الثاني. يشار إلى قاعدة البيانات ، وهذه هي قاعدة بيانات الاختبار التي نتصل بها.

Classid يشير إلى الجدول مساحة اسم الموقع الذي يحتوي على المخططات.

 => SELECT nspname FROM pg_namespace WHERE oid = 2200; 
  nspname --------- public (1 row) 

وبالتالي ، يتم حظر المخطط العام.

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

 => ROLLBACK; 

تمديد قفل العلاقة


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

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

بالطبع ، يتم تحرير هذا القفل دون انتظار نهاية المعاملة.

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

قفل الصفحة


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

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

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

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

الأقفال الاستشارية


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

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

 => SELECT hashtext('1'); 
  hashtext ----------- 243773337 (1 row) 

هكذا نلتقط القفل:

 => BEGIN; => SELECT pg_advisory_lock(hashtext('1')); 

كالعادة ، تتوفر معلومات القفل في pg_locks:

 => SELECT locktype, objid, mode, granted FROM pg_locks WHERE locktype = 'advisory' AND pid = pg_backend_pid(); 
  locktype | objid | mode | granted ----------+-----------+---------------+--------- advisory | 243773337 | ExclusiveLock | t (1 row) 

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

في المثال أعلاه ، يكون القفل ساري المفعول حتى نهاية الجلسة ، وليس المعاملة ، كالمعتاد.

 => COMMIT; => SELECT locktype, objid, mode, granted FROM pg_locks WHERE locktype = 'advisory' AND pid = pg_backend_pid(); 
  locktype | objid | mode | granted ----------+-----------+---------------+--------- advisory | 243773337 | ExclusiveLock | t (1 row) 

يجب أن يتم إصدارها بشكل صريح:

 => SELECT pg_advisory_unlock(hashtext('1')); 

هناك مجموعة كبيرة من الوظائف للعمل مع الأقفال الاستشارية لجميع المناسبات:

  • pg_advisory_lock_shared يعامل قفل مشترك ،
  • يحصل pg_advisory_xact_lock (و pg_advisory_xact_lock_shared) على قفل حتى نهاية المعاملة ،
  • لا يتوقع pg_try_advisory_lock (وكذلك pg_try_advisory_xact_lock و pg_try_advisory_xact_lock_shared) استلام القفل ، لكن يُرجع قيمة خاطئة إذا تعذر الحصول على القفل على الفور.

توفر مجموعة وظائف try طريقة أخرى لعدم انتظار القفل ، بالإضافة إلى تلك المدرجة في مقالة سابقة .

أقفال المسند


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

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

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

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

نحن مهتمون بنوعين من التبعيات:

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

يمكن تتبع تبعيات WR باستخدام الأقفال التقليدية الحالية ، ولكن يجب أن تتبع التبعيات RW فقط لتتبع.

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

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

 => CREATE TABLE pred(n integer); => INSERT INTO pred(n) SELECT gn FROM generate_series(1,10000) g(n); => CREATE INDEX ON pred(n) WITH (fillfactor = 10); => ANALYZE pred; 

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

 | => SELECT pg_backend_pid(); 
 | pg_backend_pid | ---------------- | 12763 | (1 row) 

 | => BEGIN ISOLATION LEVEL SERIALIZABLE; | => EXPLAIN (analyze, costs off) | SELECT * FROM pred WHERE n > 100; 
 | QUERY PLAN | ---------------------------------------------------------------- | Seq Scan on pred (actual time=0.047..12.709 rows=9900 loops=1) | Filter: (n > 100) | Rows Removed by Filter: 100 | Planning Time: 0.190 ms | Execution Time: 15.244 ms | (5 rows) 

يتم دائمًا التقاط أي أقفال مسند في وضع SIReadLock (قراءة متسلسلة معزولة) واحد خاص:

 => SELECT locktype, relation::regclass, page, tuple FROM pg_locks WHERE mode = 'SIReadLock' AND pid = 12763; 
  locktype | relation | page | tuple ----------+----------+------+------- relation | pred | | (1 row) 

 | => ROLLBACK; 

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

 | => BEGIN ISOLATION LEVEL SERIALIZABLE; | => EXPLAIN (analyze, costs off) | SELECT * FROM pred WHERE n BETWEEN 1000 AND 1001; 
 | QUERY PLAN | ------------------------------------------------------------------------------------ | Index Only Scan using pred_n_idx on pred (actual time=0.122..0.131 rows=2 loops=1) | Index Cond: ((n >= 1000) AND (n <= 1001)) | Heap Fetches: 2 | Planning Time: 0.096 ms | Execution Time: 0.153 ms | (5 rows) 

 => SELECT locktype, relation::regclass, page, tuple FROM pg_locks WHERE mode = 'SIReadLock' AND pid = 12763; 
  locktype | relation | page | tuple ----------+------------+------+------- tuple | pred | 3 | 236 tuple | pred | 3 | 235 page | pred_n_idx | 22 | (3 rows) 

قد تلاحظ العديد من الصعوبات.

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

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

 => SHOW max_pred_locks_per_page; 
  max_pred_locks_per_page ------------------------- 2 (1 row) 

 | => EXPLAIN (analyze, costs off) | SELECT * FROM pred WHERE n BETWEEN 1000 AND 1002; 
 | QUERY PLAN | ------------------------------------------------------------------------------------ | Index Only Scan using pred_n_idx on pred (actual time=0.019..0.039 rows=3 loops=1) | Index Cond: ((n >= 1000) AND (n <= 1002)) | Heap Fetches: 3 | Planning Time: 0.069 ms | Execution Time: 0.057 ms | (5 rows) 

بدلاً من ثلاثة أقفال tuple ، نرى نوع صفحة واحد:

 => SELECT locktype, relation::regclass, page, tuple FROM pg_locks WHERE mode = 'SIReadLock' AND pid = 12763; 
  locktype | relation | page | tuple ----------+------------+------+------- page | pred | 3 | page | pred_n_idx | 22 | (2 rows) 

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

لا توجد مستويات أخرى: يتم التقاط الأقفال الأصلية فقط للعلاقات أو الصفحات أو إصدارات الصف ، ودائمًا مع وضع SIReadLock.

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

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

 => INSERT INTO pred SELECT 1001 FROM generate_series(1,1000); => SELECT locktype, relation::regclass, page, tuple FROM pg_locks WHERE mode = 'SIReadLock' AND pid = 12763; 
  locktype | relation | page | tuple ----------+------------+------+------- page | pred | 3 | page | pred_n_idx | 211 | page | pred_n_idx | 212 | page | pred_n_idx | 22 | (4 rows) 

 | => ROLLBACK; 

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

لا تدعم جميع أنواع الفهرس في PostgreSQL الأقفال الأصلية. في السابق ، كان بإمكان B-tree فقط التباهي بهذا ، ولكن في PostgreSQL 11 ، تحسن الوضع: تمت إضافة فهارس التجزئة و GiST و GIN إلى القائمة. إذا تم استخدام الوصول إلى الفهرس ، وكان الفهرس لا يعمل مع الأقفال الأصلية ، فسيتم قفل الفهرس بالكامل على القفل. بالطبع ، هذا يزيد أيضًا من عدد فواصل المعاملات غير الصحيحة.

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

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

أن تستمر .

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


All Articles