MVCC في PostgreSQL-4. لقطات

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

سنبحث الآن في كيفية الحصول على لقطات بيانات متسقة من المجموعات.

ما هي لقطة البيانات؟


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

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

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



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



رؤية tuples في لقطة


قواعد الرؤية


لقطة ليست بالتأكيد نسخة مادية من جميع tuples اللازمة. يتم تحديد لقطة في الواقع من قبل عدة أرقام ، ويتم تحديد مدى رؤية tuples في لقطة من القواعد.

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

قواعد الرؤية الدقيقة معقدة للغاية وتراعي الكثير من الحالات والتطورات المختلفة.
يمكنك التأكد بسهولة من ذلك من خلال البحث في src / backend / utils / time / tqual.c (في الإصدار 12 ، تم نقل الشيك إلى src / backend / access / heap / heapam_visibility.c).

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

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

يمكننا تمثيل المعاملات حسب القطاعات بيانياً (من وقت البدء إلى وقت الالتزام):



هنا:

  • ستكون تغييرات المعاملة 2 مرئية منذ اكتمالها قبل إنشاء اللقطة.
  • لن تكون تغييرات المعاملة 1 مرئية لأنها كانت نشطة في اللحظة التي تم فيها إنشاء اللقطة.
  • لن تكون تغييرات المعاملة 3 مرئية منذ أن بدأت بعد إنشاء اللقطة (بغض النظر عما إذا كانت قد اكتملت أم لا).

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

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

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

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

لذلك ، يتم تحديد لقطة من قبل العديد من المعلمات:

  • في اللحظة التي تم فيها إنشاء اللقطة ، بمعنى أكثر دقة ، معرف المعاملة التالية ، ولكنه غير متوفر في النظام ( snapshot.xmax ).
  • قائمة المعاملات النشطة (قيد التقدم) في لحظة إنشاء snapshot.xip ( snapshot.xip ).

من أجل الراحة والتحسين ، يتم أيضًا تخزين معرّف أول معاملة نشطة ( snapshot.xmin ). هذه القيمة لها معنى مهم ، والتي سيتم مناقشتها أدناه.

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



مثال


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

  • تمت إضافة الأولى بواسطة معاملة بدأت قبل إنشاء اللقطة ولكنها اكتملت بعدها.
  • تمت إضافة الثانية بمعاملة بدأت واستكملت قبل إنشاء اللقطة.
  • تمت إضافة الثالث بعد إنشاء لقطة.

 => TRUNCATE TABLE accounts; 

المعاملة الأولى (لم تكتمل بعد):

 => BEGIN; => INSERT INTO accounts VALUES (1, '1001', 'alice', 1000.00); => SELECT txid_current(); 
 => SELECT txid_current(); txid_current -------------- 3695 (1 row) 

المعاملة الثانية (اكتملت قبل إنشاء اللقطة):

 | => BEGIN; | => INSERT INTO accounts VALUES (2, '2001', 'bob', 100.00); | => SELECT txid_current(); 
 | txid_current | -------------- | 3696 | (1 row) 
 | => COMMIT; 

إنشاء لقطة في معاملة في جلسة أخرى.

 || => BEGIN ISOLATION LEVEL REPEATABLE READ; || => SELECT xmin, xmax, * FROM accounts; 
 || xmin | xmax | id | number | client | amount || ------+------+----+--------+--------+-------- || 3696 | 0 | 2 | 2001 | bob | 100.00 || (1 row) 

تنفيذ المعاملة الأولى بعد إنشاء اللقطة:

 => COMMIT; 

والمعاملة الثالثة (ظهرت بعد إنشاء اللقطة):

 | => BEGIN; | => INSERT INTO accounts VALUES (3, '2002', 'bob', 900.00); | => SELECT txid_current(); 
 | txid_current | -------------- | 3697 | (1 row) 
 | => COMMIT; 

من الواضح ، صف واحد فقط لا يزال مرئيًا في لقطة لدينا:

 || => SELECT xmin, xmax, * FROM accounts; 
 || xmin | xmax | id | number | client | amount || ------+------+----+--------+--------+-------- || 3696 | 0 | 2 | 2001 | bob | 100.00 || (1 row) 

والسؤال هو كيف يفهم بوستجرس هذا.

يتم تحديد كل شيء عن طريق لقطة. دعونا ننظر في الأمر:

 || => SELECT txid_current_snapshot(); 
 || txid_current_snapshot || ----------------------- || 3695:3697:3695 || (1 row) 

ستجد هنا snapshot.xmin و snapshot.xmax و snapshot.xip مدرجة ومحددة بعلامة النقطتين ( snapshot.xip هو رقم واحد في هذه الحالة ، ولكن بصفة عامة هي قائمة).

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

 => SELECT xmin, xmax, * FROM accounts ORDER BY id; 
  xmin | xmax | id | number | client | amount ------+------+----+--------+--------+--------- 3695 | 0 | 1 | 1001 | alice | 1000.00 3696 | 0 | 2 | 2001 | bob | 100.00 3697 | 0 | 3 | 2002 | bob | 900.00 (3 rows) 

الصف الأول غير مرئي: تم إنشاؤه بواسطة معاملة مدرجة في قائمة المعاملات النشطة ( xip ).
الصف الثاني مرئي: تم إنشاؤه بواسطة معاملة موجودة في نطاق اللقطة.
الصف الثالث غير مرئي: تم إنشاؤه بواسطة معاملة خارج نطاق اللقطة.

 || => COMMIT; 

التغييرات المعاملة الخاصة


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

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

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

هنا مثال بسيط. لنبدأ معاملة ونضيف صفًا إلى الجدول:

 => BEGIN; => SELECT txid_current(); 
  txid_current -------------- 3698 (1 row) 
 INSERT INTO accounts(id, number, client, amount) VALUES (4, 3001, 'charlie', 100.00); 

دعنا cmin محتويات الجدول ، إلى جانب حقل cmin (ولكن فقط للصفوف التي تضيفها المعاملة - بالنسبة للآخرين لا معنى لها):

 => SELECT xmin, CASE WHEN xmin = 3698 THEN cmin END cmin, * FROM accounts; 
  xmin | cmin | id | number | client | amount ------+------+----+--------+---------+--------- 3695 | | 1 | 1001 | alice | 1000.00 3696 | | 2 | 2001 | bob | 100.00 3697 | | 3 | 2002 | bob | 900.00 3698 | 0 | 4 | 3001 | charlie | 100.00 (4 rows) 

نفتح الآن مؤشرًا لاستعلام يُرجع عدد الصفوف في الجدول.

 => DECLARE c CURSOR FOR SELECT count(*) FROM accounts; 

وبعد ذلك نضيف صفًا آخر:

 => INSERT INTO accounts(id, number, client, amount) VALUES (5, 3002, 'charlie', 200.00); 

إرجاع الاستعلام 4 - الصف الذي تمت إضافته بعد فتح المؤشر لا يدخل في لقطة البيانات:

 => FETCH c; 
  count ------- 4 (1 row) 

لماذا؟ لأن اللقطة تأخذ في الاعتبار tuples فقط مع cmin < 1 .

 => SELECT xmin, CASE WHEN xmin = 3698 THEN cmin END cmin, * FROM accounts; 
  xmin | cmin | id | number | client | amount ------+------+----+--------+---------+--------- 3695 | | 1 | 1001 | alice | 1000.00 3696 | | 2 | 2001 | bob | 100.00 3697 | | 3 | 2002 | bob | 900.00 3698 | 0 | 4 | 3001 | charlie | 100.00 3698 | 1 | 5 | 3002 | charlie | 200.00 (5 rows) 
 => ROLLBACK; 

أفق الحدث


يكون لمعرف أقدم معاملة نشطة ( snapshot.xmin ) معنىً هامًا: فهو يحدد "أفق الحدث" للمعاملة. أي أنه خارج الأفق ، لا ترى المعاملات دائمًا سوى إصدارات الصف المحدثة.

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



يمكنك رؤية أفق المعاملة في كتالوج النظام:

 => BEGIN; => SELECT backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid(); 
  backend_xmin -------------- 3699 (1 row) 

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

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

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

إذا جعلنا الآن شريحة تمثل لقطات (من snapshot.xmin إلى snapshot.xmax ) بدلاً من المعاملات ، يمكننا تصور الموقف على النحو التالي:



في هذا الشكل ، تتعلق أدنى لقطة بمعاملة غير مكتملة ، وفي اللقطات الأخرى ، لا يمكن أن يكون snapshot.xmin أكبر من معرف المعاملة.

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

 | => BEGIN; | => UPDATE accounts SET amount = amount + 1.00; | => COMMIT; 
 => SELECT backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid(); 
  backend_xmin -------------- 3699 (1 row) 

وفقط بعد إتمام الصفقة ، يتحرك الأفق للأمام ، مما يتيح تفريغ المضاعفات الميتة بعيدًا:

 => COMMIT; => SELECT backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid(); 
  backend_xmin -------------- 3700 (1 row) 

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

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

تصدير لقطة


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

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

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

 => BEGIN ISOLATION LEVEL REPEATABLE READ; => SELECT count(*) FROM accounts; -- any query 
  count ------- 3 (1 row) 
 => SELECT pg_export_snapshot(); 
  pg_export_snapshot --------------------- 00000004-00000E7B-1 (1 row) 

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

 | => DELETE FROM accounts; | => BEGIN ISOLATION LEVEL REPEATABLE READ; | => SET TRANSACTION SNAPSHOT '00000004-00000E7B-1'; 

ستعمل المعاملة الثانية الآن مع لقطة الأولى ، وبالتالي ، انظر ثلاثة صفوف (بدلاً من الصفر):

 | => SELECT count(*) FROM accounts; 
 | count | ------- | 3 | (1 row) 

عمر اللقطة التي تم تصديرها هو نفس عمر المعاملة المصدرة.

 | => COMMIT; => COMMIT; 

اقرأ على .

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


All Articles