MVCC-4. لقطات البيانات

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

ننظر اليوم إلى كيفية الحصول على إصدارات متسقة من البيانات من إصدارات الصفوف.

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


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

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

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



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



رؤية إصدارات الصف في لقطة


قواعد الرؤية


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

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

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

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

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

من الممكن تصوير المعاملات بيانيا في شكل قطاعات (من البداية إلى وقت الالتزام):



هنا:

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

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

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

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

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

  • لحظة إنشاء اللقطة ، أي رقم المعاملة التالية غير الموجودة في النظام ( snapshot.xmax ) ؛
  • قائمة بالمعاملات النشطة في وقت التقاط اللقطة ( 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.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 الرقم المراد حذفه ، ولكن لتوفير مساحة في رأس السطر ، هذا في الواقع حقل واحد ، وليس حقلين مختلفين. يُعتقد أن إدراج وحذف نفس الصف في معاملة واحدة أمر نادر الحدوث.

إذا استمر حدوث ذلك ، فسيتم إدخال رقم "تحرير وسرد" خاص في نفس الحقل ، حيث تتذكر عملية الخدمة حول cmin و cmax الحقيقي. ولكن هذا غريب تماما.

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

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

سنقوم بعرض محتويات الجدول مع حقل 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) 

لماذا؟ لأنه في اللقطة ، تؤخذ فقط إصدارات الخطوط مع 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 الأقدم. سيحدد الأفق الذي لن تظهر بعده الإصدارات غير الملائمة من الصفوف في قاعدة البيانات هذه لأي معاملات. يمكن مسح هذه الإصدارات من السلاسل - وهذا هو السبب في أن مفهوم الأفق مهم للغاية من وجهة نظر عملية.

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

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

إذا تم عرض الآن ، وليس معاملة ، ولكن اللقطات (من snapshot.xmin إلى snapshot.xmax) كقطعة ، يمكن تصور الموقف على النحو التالي:



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

في مثالنا ، تم بدء معاملة بمستوى عزل 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_export_snapshot معرف اللقطة التي يمكن نقلها (بواسطة وسائل خارجية إلى DBMS) إلى معاملة أخرى.

 => BEGIN ISOLATION LEVEL REPEATABLE READ; => SELECT count(*) FROM accounts; --   
  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/ar446652/


All Articles