أقفال PostgreSQL: 2. أقفال السلسلة

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



أقفال الصف


جهاز


اسمحوا لي أن أذكرك بالعديد من الاستنتاجات المهمة من المقال الأخير.

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

نحن بالتأكيد نريد تغيير صف واحد لمنع الصفوف الأخرى من نفس الجدول. لكن لا يمكننا أن نبدأ في كل سطر بقفلنا الخاص.

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

كما سنرى لاحقًا ، يستخدم PostgreSQL أيضًا هذه الآلية ، ولكن فقط للأقفال الأصلية. أقفال الخط مختلفة.

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

الإيجابيات هي أنه يمكننا حظر العديد من الأسطر التي نرغب دون استهلاك أي موارد.

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

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

أوضاع استثنائية


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

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

يحدد الأمر UPDATE نفسه وضع القفل المناسب الأدنى ؛ عادةً يتم تأمين الصفوف في وضع "عدم تحديث مفتاح".

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

لنرى. قم بإنشاء جدول حسابات ، كما في المقالة السابقة.

=> CREATE TABLE accounts( acc_no integer PRIMARY KEY, amount numeric ); => INSERT INTO accounts VALUES (1, 100.00), (2, 200.00), (3, 300.00); 

لإلقاء نظرة على الصفحات ، بالطبع ، نحن بحاجة إلى ملحق pageinspect المألوف بالفعل.

 => CREATE EXTENSION pageinspect; 

للراحة ، قم بإنشاء طريقة عرض تُظهر فقط المعلومات التي نهتم بها: xmax وبعض وحدات البت في المعلومات.

 => CREATE VIEW accounts_v AS SELECT '(0,'||lp||')' AS ctid, t_xmax as xmax, CASE WHEN (t_infomask & 128) > 0 THEN 't' END AS lock_only, CASE WHEN (t_infomask & 4096) > 0 THEN 't' END AS is_multi, CASE WHEN (t_infomask2 & 8192) > 0 THEN 't' END AS keys_upd, CASE WHEN (t_infomask & 16) > 0 THEN 't' END AS keyshr_lock, CASE WHEN (t_infomask & 16+64) = 16+64 THEN 't' END AS shr_lock FROM heap_page_items(get_raw_page('accounts',0)) ORDER BY lp; 

لذلك ، نبدأ المعاملة ونحدّث مبلغ الحساب الأول (لا يتغير المفتاح) ورقم الحساب الثاني (يتغير المفتاح):

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

نحن ننظر إلى الرأي:

 => SELECT * FROM accounts_v LIMIT 2; 
  ctid | xmax | lock_only | is_multi | keys_upd | keyshr_lock | shr_lock -------+--------+-----------+----------+----------+-------------+---------- (0,1) | 530492 | | | | | (0,2) | 530492 | | | t | | (2 rows) 

يتم تحديد وضع القفل بواسطة بت المعلومات key_updated.

يتم استخدام نفس حقل xmax أيضًا عند قفل صف بأمر SELECT FOR UPDATE ، ولكن في هذه الحالة ، يتم وضع بت معلومات إضافية (xmax_lock_only) ، مما يشير إلى أن إصدار الصف مقفل فقط ، ولكن لا يتم حذفه ولا يزال صالحًا.

 => ROLLBACK; => BEGIN; => SELECT * FROM accounts WHERE acc_no = 1 FOR NO KEY UPDATE; => SELECT * FROM accounts WHERE acc_no = 2 FOR UPDATE; 

 => SELECT * FROM accounts_v LIMIT 2; 
  ctid | xmax | lock_only | is_multi | keys_upd | keyshr_lock | shr_lock -------+--------+-----------+----------+----------+-------------+---------- (0,1) | 530493 | t | | | | (0,2) | 530493 | t | | t | | (2 rows) 

 => ROLLBACK; 


أوضاع مشتركة


يمثل وضعان آخران الأقفال المشتركة التي يمكن أن تعقدها عدة معاملات.

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

لنرى.

 => BEGIN; => SELECT * FROM accounts WHERE acc_no = 1 FOR KEY SHARE; => SELECT * FROM accounts WHERE acc_no = 2 FOR SHARE; 

في إصدارات الصف نرى:

 => SELECT * FROM accounts_v LIMIT 2; 
  ctid | xmax | lock_only | is_multi | keys_upd | keyshr_lock | shr_lock -------+--------+-----------+----------+----------+-------------+---------- (0,1) | 530494 | t | | | t | (0,2) | 530494 | t | | | t | t (2 rows) 

في كلتا الحالتين ، يتم تعيين بت keyshr_lock ، ويمكن التعرف على وضع SHARE من خلال النظر في واحد بت مزيد من المعلومات.

إليك ما تبدو عليه مصفوفة توافق الوضع العام.

نظام الحكمللسهم الرئيسيللسهملا يوجد تحديث رئيسيللتحديث
للسهم الرئيسيX
للسهمXX
لا يوجد تحديث رئيسيXXX
للتحديثXXXX

إنه يبين أن:

  • أوضاع استثنائية تتعارض مع بعضها البعض ؛
  • أوضاع مشتركة متوافقة مع بعضها البعض.
  • يتوافق وضع FOR KEY SHARE المشترك مع الوضع FOR FOR KEY UPDATE الحصري (أي ، يمكنك تحديث الحقول غير الرئيسية في وقت واحد والتأكد من أن المفتاح لا يتغير).

Multitranzaktsii


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

بالنسبة للأقفال المشتركة ، يتم استخدام ما يسمى بالمعاملات المتعددة (MultiXact). هذه هي مجموعة المعاملات التي تم تعيين رقم منفصل. هذا الرقم له نفس البعد كرقم معاملة عادي ، ولكن يتم تخصيص الأرقام بشكل مستقل (أي ، يمكن أن يحتوي النظام على نفس المعاملة وأرقام المعاملات المتعددة). لتمييز أحدهما عن الآخر ، يتم استخدام بت معلومات آخر (xmax_is_multi) ، وتوجد معلومات مفصلة حول أعضاء هذه المجموعة وأوضاع القفل في الملفات الموجودة في الدليل $ PGDATA / pg_multixact /. بطبيعة الحال ، يتم تخزين البيانات الأخيرة المستخدمة في المخازن المؤقتة في الذاكرة المشتركة للخادم للوصول بشكل أسرع.

أضف إلى الأقفال الحالية واحدة استثنائية أخرى تنفذها معاملة أخرى (يمكننا القيام بذلك ، لأن أوضاع FOR KEY SHARE و FOR NO KEY UPDATE متوافقة مع بعضها البعض):

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

 => SELECT * FROM accounts_v LIMIT 2; 
  ctid | xmax | lock_only | is_multi | keys_upd | keyshr_lock | shr_lock -------+--------+-----------+----------+----------+-------------+---------- (0,1) | 61 | | t | | | (0,2) | 530494 | t | | | t | t (2 rows) 

في السطر الأول ، نرى أن الرقم المعتاد قد تم استبداله برقم متعدد النقل - تتم الإشارة إلى ذلك بواسطة xmax_is_multi bit.

من أجل عدم الخوض في الأجزاء الداخلية لتطبيق multitransaction ، يمكنك استخدام ملحق آخر يتيح لك رؤية جميع المعلومات حول جميع أنواع أقفال الصفوف بطريقة مريحة.

 => CREATE EXTENSION pgrowlocks; => SELECT * FROM pgrowlocks('accounts') \gx 
 -[ RECORD 1 ]----------------------------- locked_row | (0,1) locker | 61 multi | t xids | {530494,530495} modes | {"Key Share","No Key Update"} pids | {5892,5928} -[ RECORD 2 ]----------------------------- locked_row | (0,2) locker | 530494 multi | f xids | {530494} modes | {"For Share"} pids | {5892} 

 => COMMIT; 

 | => ROLLBACK; 

تجميد الإعداد


نظرًا لأنه يتم تخصيص أرقام منفصلة للعمليات المتعددة ، والتي تتم كتابتها في حقل xmax الخاص بإصدارات الصف ، نظرًا لوجود الحد الأقصى لسعة الأرقام في العداد ، فإنها تواجه نفس مشكلة التفاف ملف التعريف xid كما هو الحال مع الرقم العادي.

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

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

بالنسبة لتجميد المعاملات المتعددة ، تكون المعلمات المشابهة لمعلمات التجميد المعتادة مسؤولة : vacuum_multixact_freeze_min_age ، vacuum_multixact_freeze_table_age ، autov Vacuum_multixact_freeze_max_age .

من هو المتطرف؟


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

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

 => CREATE VIEW locks_v AS SELECT pid, locktype, CASE locktype WHEN 'relation' THEN relation::regclass::text WHEN 'transactionid' THEN transactionid::text WHEN 'tuple' THEN relation::regclass::text||':'||tuple::text END AS lockid, mode, granted FROM pg_locks WHERE locktype in ('relation','transactionid','tuple') AND (locktype != 'relation' OR relation = 'accounts'::regclass); 

الآن ابدأ المعاملة الأولى وقم بتحديث الصف.

 => BEGIN; => SELECT txid_current(), pg_backend_pid(); 
  txid_current | pg_backend_pid --------------+---------------- 530497 | 5892 (1 row) 
 => UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 1; 
 UPDATE 1 

ماذا عن الأقفال؟

 => SELECT * FROM locks_v WHERE pid = 5892; 
  pid | locktype | lockid | mode | granted ------+---------------+----------+------------------+--------- 5892 | relation | accounts | RowExclusiveLock | t 5892 | transactionid | 530497 | ExclusiveLock | t (2 rows) 

الصفقة تحمل الجدول وأقفال عدد الخاصة. حتى الآن ، كل شيء متوقع.

نبدأ المعاملة الثانية ونحاول تحديث نفس السطر.

 | => BEGIN; | => SELECT txid_current(), pg_backend_pid(); 
 | txid_current | pg_backend_pid | --------------+---------------- | 530498 | 5928 | (1 row) 
 | => UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 1; 

ماذا عن أقفال المعاملة الثانية؟

 => SELECT * FROM locks_v WHERE pid = 5928; 
  pid | locktype | lockid | mode | granted ------+---------------+------------+------------------+--------- 5928 | relation | accounts | RowExclusiveLock | t 5928 | transactionid | 530498 | ExclusiveLock | t 5928 | transactionid | 530497 | ShareLock | f 5928 | tuple | accounts:1 | ExclusiveLock | t (4 rows) 

وهنا هو أكثر إثارة للاهتمام. بالإضافة إلى قفل الجدول والرقم الخاص ، نرى قفلين آخرين. وجدت المعاملة الثانية أنه تم قفل الصف أولاً و "تعليقه" في انتظار رقمه (الممنوح = f). ولكن من أين ولماذا أتى قفل إصدار السطر (locktype = tuple)؟

لا تخلط بين قفل إصدار الصف (قفل الصفوف) وقفل الصف (قفل الصف). الأول هو قفل نوع tuple منتظم ، وهو مرئي في pg_locks. والثاني هو علامة في صفحة البيانات: xmax وبتات المعلومات.

عندما توشك إحدى المعاملات على تغيير صف ما ، فإنها تنفذ تسلسل الإجراءات التالي:

  1. يلتقط قفلًا حصريًا على نسخة قابلة للتغيير من السلسلة (tuple).
  2. إذا أشارت xmax وبتات المعلومات إلى أن الخط مؤمن ، فسيطلب منك تأمين رقم المعاملة xmax.
  3. يصف xmax وبتات المعلومات الضرورية.
  4. إصدارات قفل نسخة الصف.

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

عندما وصلت المعاملة الثانية ، ألقت القبض على قفل إصدار الصف (البند 1) ، لكنها اضطرت إلى طلب قفل على رقم المعاملة الأولى (البند 2) وعلقتها.

ماذا يحدث إذا ظهرت معاملة مماثلة ثالثة؟ ستحاول التقاط قفل إصدار السطر (البند 1) وستعلق بالفعل في هذه الخطوة. التحقق من ذلك.

 || => BEGIN; || => SELECT txid_current(), pg_backend_pid(); 
 || txid_current | pg_backend_pid || --------------+---------------- || 530499 | 5964 || (1 row) 
 || => UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 1; 

 => SELECT * FROM locks_v WHERE pid = 5964; 
  pid | locktype | lockid | mode | granted ------+---------------+------------+------------------+--------- 5964 | relation | accounts | RowExclusiveLock | t 5964 | tuple | accounts:1 | ExclusiveLock | f 5964 | transactionid | 530499 | ExclusiveLock | t (3 rows) 

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

إضافة معاملة أخرى إلى الكومة.

 ||| => BEGIN; ||| => SELECT txid_current(), pg_backend_pid(); 
 ||| txid_current | pg_backend_pid ||| --------------+---------------- ||| 530500 | 6000 ||| (1 row) 
 ||| => UPDATE accounts SET amount = amount - 100.00 WHERE acc_no = 1; 

 => SELECT * FROM locks_v WHERE pid = 6000; 
  pid | locktype | lockid | mode | granted ------+---------------+------------+------------------+--------- 6000 | relation | accounts | RowExclusiveLock | t 6000 | transactionid | 530500 | ExclusiveLock | t 6000 | tuple | accounts:1 | ExclusiveLock | f (3 rows) 

يمكن رؤية صورة عامة للتوقعات الحالية في طريقة العرض pg_stat_activity ، مع إضافة معلومات حول عمليات الحظر:

 => SELECT pid, wait_event_type, wait_event, pg_blocking_pids(pid) FROM pg_stat_activity WHERE backend_type = 'client backend'; 
  pid | wait_event_type | wait_event | pg_blocking_pids ------+-----------------+---------------+------------------ 5892 | | | {} 5928 | Lock | transactionid | {5892} 5964 | Lock | tuple | {5928} 6000 | Lock | tuple | {5928,5964} (4 rows) 

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

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

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

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

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

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

السماح للمعاملة الأولى كاملة مع الالتزام.

 => COMMIT; 

سيتم إيقاظ المعاملة الثانية وتنفيذ الفقرات. 3 و 4.

 | UPDATE 1 

 => SELECT * FROM locks_v WHERE pid = 5928; 
  pid | locktype | lockid | mode | granted ------+---------------+----------+------------------+--------- 5928 | relation | accounts | RowExclusiveLock | t 5928 | transactionid | 530498 | ExclusiveLock | t (2 rows) 

ماذا عن الصفقة الثالثة؟ تتخطى الخطوة 1 (لأن المورد قد اختفى) وتعطلت في الخطوة 2:

 => SELECT * FROM locks_v WHERE pid = 5964; 
  pid | locktype | lockid | mode | granted ------+---------------+----------+------------------+--------- 5964 | relation | accounts | RowExclusiveLock | t 5964 | transactionid | 530498 | ShareLock | f 5964 | transactionid | 530499 | ExclusiveLock | t (3 rows) 

ونفس الشيء يحدث مع الصفقة الرابعة:

 => SELECT * FROM locks_v WHERE pid = 6000; 
  pid | locktype | lockid | mode | granted ------+---------------+----------+------------------+--------- 6000 | relation | accounts | RowExclusiveLock | t 6000 | transactionid | 530498 | ShareLock | f 6000 | transactionid | 530500 | ExclusiveLock | t (3 rows) 

وهذا هو ، كل من المعاملات الثالثة والرابعة تنتظر الانتهاء من الثانية. تحول الخط إلى حشد اليقطين .

نكمل جميع المعاملات التي بدأت.

 | => COMMIT; 

 || UPDATE 1 
 || => COMMIT; 

 ||| UPDATE 1 
 ||| => COMMIT; 

يمكن العثور على مزيد من التفاصيل حول حظر السلاسل في README.tuplock .

لم تكن تقف هنا


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

دع المعاملة الأولى تغلق الصف في الوضع المشترك.

 => BEGIN; => SELECT txid_current(), pg_backend_pid(); 
  txid_current | pg_backend_pid --------------+---------------- 530501 | 5892 (1 row) 
 => SELECT * FROM accounts WHERE acc_no = 1 FOR SHARE; 
  acc_no | amount --------+-------- 1 | 100.00 (1 row) 

تحاول المعاملة الثانية تحديث نفس الصف ، لكن لا يمكنها - أوضاع SHARE و NO KEY UPDATE غير متوافقة.

 | => BEGIN; | => SELECT txid_current(), pg_backend_pid(); 
 | txid_current | pg_backend_pid | --------------+---------------- | 530502 | 5928 | (1 row) 
 | => UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 1; 

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

 => SELECT * FROM locks_v WHERE pid = 5928; 
  pid | locktype | lockid | mode | granted ------+---------------+-------------+------------------+--------- 5928 | relation | accounts | RowExclusiveLock | t 5928 | tuple | accounts:10 | ExclusiveLock | t 5928 | transactionid | 530501 | ShareLock | f 5928 | transactionid | 530502 | ExclusiveLock | t (4 rows) 

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

 || BEGIN || => SELECT txid_current(), pg_backend_pid(); 
 || txid_current | pg_backend_pid || --------------+---------------- || 530503 | 5964 || (1 row) 
 || => SELECT * FROM accounts WHERE acc_no = 1 FOR SHARE; 
 || acc_no | amount || --------+-------- || 1 | 100.00 || (1 row) 

والآن هناك معاملات تمنع الصف:

 => SELECT * FROM pgrowlocks('accounts') \gx 
 -[ RECORD 1 ]--------------- locked_row | (0,10) locker | 62 multi | t xids | {530501,530503} modes | {Share,Share} pids | {5892,5964} 

ماذا يحدث الآن عند اكتمال المعاملة الأولى؟ سيتم إيقاظ المعاملة الثانية ، ولكن سترى أن قفل الصف لم يختف في أي مكان ، وسوف يقف مرة أخرى في "قائمة الانتظار" - هذه المرة للمعاملة الثالثة:

 => COMMIT; => SELECT * FROM locks_v WHERE pid = 5928; 
  pid | locktype | lockid | mode | granted ------+---------------+-------------+------------------+--------- 5928 | relation | accounts | RowExclusiveLock | t 5928 | tuple | accounts:10 | ExclusiveLock | t 5928 | transactionid | 530503 | ShareLock | f 5928 | transactionid | 530502 | ExclusiveLock | t (4 rows) 

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

 || => COMMIT; 

 | UPDATE 1 
 | => ROLLBACK; 

ربما حان الوقت لاستخلاص بعض الاستنتاجات العملية.

  • لا يعد تحديث نفس الصف في جدول في نفس الوقت في العديد من العمليات المتوازية فكرة جيدة.
  • إذا كنت تستخدم الأقفال المشتركة من النوع SHARE في التطبيق ، فسرًّا.
  • يجب ألا يتدخل التحقق من المفاتيح الخارجية ، نظرًا لأن حقول المفاتيح لا تتغير عادة ، وأن أوضاع KEY SHARE و NO KEY UPDATE متوافقة.


طلب عدم الاقتراض


عادةً ما تتوقع أوامر SQL تحرير الموارد التي يحتاجون إليها. لكن في بعض الأحيان تريد رفض تنفيذ الأمر إذا تعذر الحصول على القفل على الفور. للقيام بذلك ، تتيح لك الأوامر مثل SELECT و LOCK و ALTER استخدام عبارة NOWAIT.

على سبيل المثال:

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

 | => SELECT * FROM accounts FOR UPDATE NOWAIT; 
 | ERROR: could not obtain lock on row in relation "accounts" 

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

لا يمكنك تحديد عبارة NOWAIT لأمرتي UPDATE و DELETE ، ولكن يمكنك أولاً تنفيذ SELECT FOR UPDATE NOWAIT ، ثم ، إذا أمكن ، قم بتحديث أو حذف السطر.

هناك خيار آخر هو عدم الانتظار - استخدم الأمر SELECT FOR مع عبارة SKIP LOCKED. مثل هذا الأمر سيتخطى الخطوط المقفلة ، لكن يعالج الخطوط المجانية.

 | => BEGIN; | => DECLARE c CURSOR FOR | SELECT * FROM accounts ORDER BY acc_no FOR UPDATE SKIP LOCKED; | => FETCH c; 
 | acc_no | amount | --------+-------- | 2 | 200.00 | (1 row) 

في هذا المثال ، تم تخطي السطر الأول المحظور وتلقينا على الفور (وحظره) الثاني.

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

 => ROLLBACK; 
 | => ROLLBACK; 

أن تستمر .

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


All Articles