MVCC-3. نسخه های رشته ای

بنابراین، ما مسائل مربوط به را در نظر گرفتیم عایق، و یک عقب نشینی در مورد سازماندهی داده ها در سطح پایین. و در نهایت به جالب ترین قسمت رسیدیم - نسخه های رشته ای.

عنوان

همانطور که قبلاً گفتیم، هر ردیف می تواند به طور همزمان در چندین نسخه در پایگاه داده وجود داشته باشد. یک نسخه باید به نحوی از نسخه دیگر متمایز شود، برای این منظور، هر نسخه دارای دو علامت است که "زمان" عملکرد این نسخه را تعیین می کند (xmin و xmax). در نقل قول - زیرا زمان به این صورت نیست که از یک شمارنده افزایش دهنده ویژه استفاده می شود. و این شمارنده شماره تراکنش است.

(طبق معمول، واقعیت پیچیده تر است: به دلیل ظرفیت بیت محدود پیشخوان، شماره تراکنش نمی تواند همیشه افزایش یابد. اما وقتی به فریز شدن رسیدیم، به جزئیات این جزئیات نگاه خواهیم کرد.)

هنگامی که یک ردیف ایجاد می شود، xmin به شماره تراکنش که دستور INSERT را صادر کرده، تنظیم می شود و xmax خالی می ماند.

هنگامی که یک ردیف حذف می شود، مقدار xmax نسخه فعلی با شماره تراکنشی که DELETE را انجام داده است، مشخص می شود.

هنگامی که یک ردیف با دستور UPDATE اصلاح می شود، در واقع دو عملیات انجام می شود: DELETE و INSERT. نسخه فعلی ردیف xmax را برابر با تعداد تراکنش‌هایی که UPDATE را انجام داده است تعیین می‌کند. سپس یک نسخه جدید از همان رشته ایجاد می شود. مقدار xmin آن با مقدار xmax نسخه قبلی مطابقت دارد.

فیلدهای xmin و xmax در هدر نسخه ردیف گنجانده شده است. علاوه بر این فیلدها، هدر شامل موارد دیگری نیز می شود، به عنوان مثال:

  • infomask یک سری بیت است که ویژگی های این نسخه را مشخص می کند. تعداد آنها بسیار زیاد است. ما به تدریج موارد اصلی را در نظر خواهیم گرفت.
  • ctid پیوندی به نسخه بعدی و جدیدتر همان خط است. برای جدیدترین و جدیدترین نسخه یک رشته، ctid به خود این نسخه اشاره دارد. عدد دارای شکل (x,y) است، جایی که x شماره صفحه است، y عدد شاخص در آرایه است.
  • null bitmap - ستون هایی از یک نسخه داده شده را که حاوی مقدار تهی (NULL) هستند، علامت گذاری می کند. NULL یکی از مقادیر معمولی نوع داده نیست، بنابراین ویژگی باید جداگانه ذخیره شود.

در نتیجه، هدر بسیار بزرگ است - حداقل 23 بایت برای هر نسخه از خط، و معمولا بیشتر به دلیل بیت مپ NULL. اگر جدول "باریک" باشد (یعنی حاوی ستون های کمی باشد)، ممکن است سربار بیش از اطلاعات مفید را اشغال کند.

قرار دادن

بیایید نگاهی دقیق‌تر به نحوه انجام عملیات رشته‌های سطح پایین بیندازیم که با درج شروع می‌شود.

برای آزمایش، بیایید یک جدول جدید با دو ستون و یک شاخص روی یکی از آنها ایجاد کنیم:

=> CREATE TABLE t(
  id serial,
  s text
);
=> CREATE INDEX ON t(s);

اجازه دهید پس از شروع یک تراکنش، یک ردیف را درج کنیم.

=> BEGIN;
=> INSERT INTO t(s) VALUES ('FOO');

این شماره تراکنش فعلی ما است:

=> SELECT txid_current();
 txid_current 
--------------
         3664
(1 row)

بیایید به محتویات صفحه نگاه کنیم. تابع heap_page_items افزونه pageinspect به شما امکان می دهد اطلاعاتی در مورد اشاره گرها و نسخه های ردیف به دست آورید:

=> SELECT * FROM heap_page_items(get_raw_page('t',0)) gx
-[ RECORD 1 ]-------------------
lp          | 1
lp_off      | 8160
lp_flags    | 1
lp_len      | 32
t_xmin      | 3664
t_xmax      | 0
t_field3    | 0
t_ctid      | (0,1)
t_infomask2 | 2
t_infomask  | 2050
t_hoff      | 24
t_bits      | 
t_oid       | 
t_data      | x0100000009464f4f

توجه داشته باشید که کلمه heap در PostgreSQL به جداول اشاره دارد. این یکی دیگر از کاربردهای عجیب این اصطلاح است - یک پشته شناخته شده است ساختار داده ها، که هیچ شباهتی به جدول ندارد. در اینجا این کلمه به معنای "همه چیز به هم ریخته شده است" در مقابل نمایه های مرتب شده استفاده می شود.

تابع داده ها را "همانطور که هست" نشان می دهد، در قالبی که درک آن دشوار است. برای فهمیدن آن، تنها بخشی از اطلاعات را باقی می گذاریم و آن را رمزگشایی می کنیم:

=> SELECT '(0,'||lp||')' AS ctid,
       CASE lp_flags
         WHEN 0 THEN 'unused'
         WHEN 1 THEN 'normal'
         WHEN 2 THEN 'redirect to '||lp_off
         WHEN 3 THEN 'dead'
       END AS state,
       t_xmin as xmin,
       t_xmax as xmax,
       (t_infomask & 256) > 0  AS xmin_commited,
       (t_infomask & 512) > 0  AS xmin_aborted,
       (t_infomask & 1024) > 0 AS xmax_commited,
       (t_infomask & 2048) > 0 AS xmax_aborted,
       t_ctid
FROM heap_page_items(get_raw_page('t',0)) gx
-[ RECORD 1 ]-+-------
ctid          | (0,1)
state         | normal
xmin          | 3664
xmax          | 0
xmin_commited | f
xmin_aborted  | f
xmax_commited | f
xmax_aborted  | t
t_ctid        | (0,1)

این چیزی است که ما انجام دادیم:

  • یک صفر به شماره فهرست اضافه کرد تا شبیه t_ctid شود: (شماره صفحه، شماره فهرست).
  • وضعیت نشانگر lp_flags را رمزگشایی کرد. در اینجا "نرمال" است - این بدان معنی است که اشاره گر در واقع به نسخه رشته اشاره دارد. بعداً معانی دیگر را بررسی خواهیم کرد.
  • از تمام بیت های اطلاعاتی، تنها دو جفت تا کنون شناسایی شده اند. بیت‌های xmin_committed و xmin_aborted نشان می‌دهند که آیا شماره تراکنش xmin متعهد است یا خیر. دو بیت مشابه به شماره تراکنش xmax اشاره دارد.

ما چه می بینیم؟ هنگامی که یک ردیف را وارد می کنید، شاخص شماره 1 در صفحه جدول ظاهر می شود که به اولین و تنها نسخه سطر اشاره می کند.

در نسخه رشته ای، فیلد xmin با شماره تراکنش فعلی پر می شود. تراکنش هنوز فعال است، بنابراین هر دو بیت xmin_committed و xmin_aborted تنظیم نشده اند.

فیلد ctid نسخه ردیف به همان ردیف اشاره دارد. این بدان معنی است که نسخه جدیدتر وجود ندارد.

فیلد xmax با یک عدد ساختگی 0 پر شده است زیرا این نسخه از ردیف حذف نشده و جاری است. تراکنش ها به این عدد توجه نمی کنند زیرا بیت xmax_aborted تنظیم شده است.

بیایید با افزودن بیت های اطلاعاتی به شماره تراکنش ها، یک قدم دیگر به سمت بهبود خوانایی برداریم. و اجازه دهید یک تابع ایجاد کنیم، زیرا به درخواست بیش از یک بار نیاز خواهیم داشت:

=> CREATE FUNCTION heap_page(relname text, pageno integer)
RETURNS TABLE(ctid tid, state text, xmin text, xmax text, t_ctid tid)
AS $$
SELECT (pageno,lp)::text::tid AS ctid,
       CASE lp_flags
         WHEN 0 THEN 'unused'
         WHEN 1 THEN 'normal'
         WHEN 2 THEN 'redirect to '||lp_off
         WHEN 3 THEN 'dead'
       END AS state,
       t_xmin || CASE
         WHEN (t_infomask & 256) > 0 THEN ' (c)'
         WHEN (t_infomask & 512) > 0 THEN ' (a)'
         ELSE ''
       END AS xmin,
       t_xmax || CASE
         WHEN (t_infomask & 1024) > 0 THEN ' (c)'
         WHEN (t_infomask & 2048) > 0 THEN ' (a)'
         ELSE ''
       END AS xmax,
       t_ctid
FROM heap_page_items(get_raw_page(relname,pageno))
ORDER BY lp;
$$ LANGUAGE SQL;

در این فرم، آنچه در هدر نسخه ردیف می‌گذرد، بسیار واضح‌تر است:

=> SELECT * FROM heap_page('t',0);
 ctid  | state  | xmin | xmax  | t_ctid 
-------+--------+------+-------+--------
 (0,1) | normal | 3664 | 0 (a) | (0,1)
(1 row)

اطلاعات مشابه، اما به میزان قابل توجهی با جزئیات کمتر، را می توان از خود جدول، با استفاده از شبه ستون های xmin و xmax به دست آورد:

=> SELECT xmin, xmax, * FROM t;
 xmin | xmax | id |  s  
------+------+----+-----
 3664 |    0 |  1 | FOO
(1 row)

تثبیت

اگر تراکنش با موفقیت انجام شود، باید وضعیت آن را به خاطر بسپارید - توجه داشته باشید که متعهد شده است. برای این کار از ساختاری به نام XACT استفاده می شود (و قبل از نسخه 10 به آن CLOG (commit log) می گفتند و این نام را هنوز در جاهای مختلف می توان یافت).

XACT یک جدول کاتالوگ سیستم نیست. این فایل‌ها در دایرکتوری PGDATA/pg_xact هستند. آنها دو بیت برای هر تراکنش دارند: تعهد و لغو - درست مانند هدر نسخه ردیف. این اطلاعات صرفاً برای سهولت به چندین فایل تقسیم شده است؛ زمانی که فریز کردن را در نظر گرفتیم به این موضوع باز خواهیم گشت. و کار با این فایل ها مانند سایر فایل ها صفحه به صفحه انجام می شود.

بنابراین، هنگامی که یک تراکنش در XACT انجام می شود، بیت متعهد برای این تراکنش تنظیم می شود. و این تمام چیزی است که در حین انجام ارتکاب اتفاق می افتد (اگرچه ما هنوز در مورد گزارش پیش ضبط صحبت نمی کنیم).

هنگامی که تراکنش دیگری به صفحه جدولی که ما به آن نگاه کردیم دسترسی پیدا می کند، باید به چندین سوال پاسخ دهد.

  1. آیا تراکنش xmin تکمیل شده است؟ اگر نه، نسخه ایجاد شده رشته نباید قابل مشاهده باشد.
    این بررسی با مشاهده ساختار دیگری انجام می شود که در حافظه مشترک نمونه قرار دارد و ProcArray نام دارد. این شامل لیستی از تمام فرآیندهای فعال است و برای هر یک تعداد تراکنش فعلی (فعال) آن مشخص شده است.
  2. اگر تکمیل شد، پس چگونه - با تعهد یا لغو؟ اگر لغو شد، نسخه ردیف نیز نباید قابل مشاهده باشد.
    این دقیقاً همان چیزی است که XACT برای آن است. اما، اگرچه آخرین صفحات XACT در بافرهای RAM ذخیره می‌شوند، باز هم هر بار بررسی XACT گران است. بنابراین، هنگامی که وضعیت تراکنش مشخص شد، در بیت های xmin_committed و xmin_aborted نسخه رشته ای نوشته می شود. اگر یکی از این بیت ها تنظیم شود، وضعیت تراکنش xmin شناخته شده در نظر گرفته می شود و تراکنش بعدی نیازی به دسترسی به XACT نخواهد داشت.

چرا این بیت ها توسط خود تراکنش تنظیم نمی شوند که درج را انجام دهند؟ هنگامی که یک درج رخ می دهد، تراکنش هنوز نمی داند که آیا موفقیت آمیز خواهد بود یا خیر. و در لحظه ارتکاب، دیگر مشخص نیست که کدام خطوط در کدام صفحه تغییر کرده است. ممکن است تعداد زیادی از این صفحات وجود داشته باشد و به خاطر سپردن آنها سودی ندارد. علاوه بر این، برخی از صفحات را می توان از کش بافر به دیسک خارج کرد. خواندن مجدد آنها برای تغییر بیت ها باعث کاهش قابل توجهی می شود.

نقطه ضعف صرفه جویی این است که پس از تغییرات، هر تراکنش (حتی تراکنشی که یک خواندن ساده انجام می دهد - SELECT) می تواند شروع به تغییر صفحات داده در حافظه پنهان بافر کند.

بنابراین، اجازه دهید تغییر را اصلاح کنیم.

=> COMMIT;

هیچ چیز در صفحه تغییر نکرده است (اما می دانیم که وضعیت تراکنش قبلاً در XACT ثبت شده است):

=> SELECT * FROM heap_page('t',0);
 ctid  | state  | xmin | xmax  | t_ctid 
-------+--------+------+-------+--------
 (0,1) | normal | 3664 | 0 (a) | (0,1)
(1 row)

اکنون تراکنشی که ابتدا به صفحه دسترسی پیدا می کند باید وضعیت تراکنش xmin را تعیین کرده و آن را در بیت های اطلاعات بنویسد:

=> SELECT * FROM t;
 id |  s  
----+-----
  1 | FOO
(1 row)

=> SELECT * FROM heap_page('t',0);
 ctid  | state  |   xmin   | xmax  | t_ctid 
-------+--------+----------+-------+--------
 (0,1) | normal | 3664 (c) | 0 (a) | (0,1)
(1 row)

حذف کنید

هنگامی که یک ردیف حذف می شود، شماره تراکنش حذف فعلی در قسمت xmax نسخه فعلی نوشته می شود و بیت xmax_aborted پاک می شود.

توجه داشته باشید که مقدار تنظیم شده xmax مربوط به تراکنش فعال به عنوان قفل ردیف عمل می کند. اگر تراکنش دیگری بخواهد این ردیف را به‌روزرسانی یا حذف کند، مجبور می‌شود منتظر تکمیل تراکنش xmax باشد. بعداً در مورد مسدود کردن بیشتر صحبت خواهیم کرد. در حال حاضر، فقط توجه می کنیم که تعداد قفل های ردیف نامحدود است. فضای رم را اشغال نمی کنند و عملکرد سیستم از تعداد آنها لطمه نمی بیند. درست است، تراکنش‌های «طولانی» معایب دیگری هم دارند، اما بعداً در مورد آن بیشتر توضیح می‌دهیم.

بیایید خط را حذف کنیم.

=> BEGIN;
=> DELETE FROM t;
=> SELECT txid_current();
 txid_current 
--------------
         3665
(1 row)

می بینیم که شماره تراکنش در قسمت xmax نوشته شده است، اما بیت های اطلاعات تنظیم نشده اند:

=> SELECT * FROM heap_page('t',0);
 ctid  | state  |   xmin   | xmax | t_ctid 
-------+--------+----------+------+--------
 (0,1) | normal | 3664 (c) | 3665 | (0,1)
(1 row)

لغو

لغو تغییرات مشابه commit کردن عمل می کند، فقط در XACT بیت قطع شده برای تراکنش تنظیم می شود. خنثی کردن به همان سرعتی است که انجام دادن. اگرچه دستور ROLLBACK نامیده می شود، اما تغییرات به عقب بازگردانده نمی شوند: هر چیزی که تراکنش موفق به تغییر در صفحات داده شده است بدون تغییر باقی می ماند.

=> ROLLBACK;
=> SELECT * FROM heap_page('t',0);
 ctid  | state  |   xmin   | xmax | t_ctid 
-------+--------+----------+------+--------
 (0,1) | normal | 3664 (c) | 3665 | (0,1)
(1 row)

هنگامی که به صفحه دسترسی پیدا کرد، وضعیت بررسی می شود و بیت xmax_aborted به نسخه ردیف تنظیم می شود. خود شماره xmax در صفحه باقی می ماند، اما هیچ کس به آن نگاه نمی کند.

=> SELECT * FROM t;
 id |  s  
----+-----
  1 | FOO
(1 row)

=> SELECT * FROM heap_page('t',0);
 ctid  | state  |   xmin   |   xmax   | t_ctid 
-------+--------+----------+----------+--------
 (0,1) | normal | 3664 (c) | 3665 (a) | (0,1)
(1 row)

بروز رسانی

به‌روزرسانی به‌گونه‌ای کار می‌کند که گویی ابتدا نسخه فعلی ردیف را حذف کرده و سپس یک نسخه جدید درج کرده است.

=> BEGIN;
=> UPDATE t SET s = 'BAR';
=> SELECT txid_current();
 txid_current 
--------------
         3666
(1 row)

پرس و جو یک خط تولید می کند (نسخه جدید):

=> SELECT * FROM t;
 id |  s  
----+-----
  1 | BAR
(1 row)

اما در صفحه ما هر دو نسخه را می بینیم:

=> SELECT * FROM heap_page('t',0);
 ctid  | state  |   xmin   | xmax  | t_ctid 
-------+--------+----------+-------+--------
 (0,1) | normal | 3664 (c) | 3666  | (0,2)
 (0,2) | normal | 3666     | 0 (a) | (0,2)
(2 rows)

نسخه حذف شده با شماره تراکنش فعلی در قسمت xmax مشخص شده است. علاوه بر این، از آنجایی که تراکنش قبلی لغو شده است، این مقدار روی مقدار قبلی نوشته می شود. و بیت xmax_aborted پاک می شود زیرا وضعیت تراکنش فعلی هنوز مشخص نیست.

نسخه اول خط اکنون به دومین (فیلد t_ctid) به عنوان جدیدتر اشاره می کند.

نمایه دوم در صفحه فهرست ظاهر می شود و ردیف دوم به نسخه دوم در صفحه جدول اشاره می کند.

درست مانند حذف، مقدار xmax در نسخه اول سطر نشان دهنده قفل بودن سطر است.

خوب، بیایید معامله را کامل کنیم.

=> COMMIT;

شاخص

تا اینجا ما فقط در مورد صفحات جدول صحبت کرده ایم. در داخل شاخص ها چه اتفاقی می افتد؟

اطلاعات در صفحات فهرست بسته به نوع خاص ایندکس بسیار متفاوت است. و حتی یک نوع ایندکس دارای انواع مختلف صفحات است. به عنوان مثال، یک درخت B دارای یک صفحه ابرداده و صفحات "عادی" است.

با این حال، صفحه معمولا دارای یک آرایه از اشاره گرها به سطرها و خود سطرها است (درست مانند یک صفحه جدول). علاوه بر این، در انتهای صفحه فضایی برای داده های ویژه وجود دارد.

سطرها در اندیس ها نیز بسته به نوع شاخص می توانند ساختارهای بسیار متفاوتی داشته باشند. به عنوان مثال، برای درخت B، ردیف های مربوط به صفحات برگ حاوی مقدار کلید نمایه سازی و یک مرجع (ctid) به ردیف جدول مربوطه است. به طور کلی، شاخص را می توان به روشی کاملاً متفاوت ساختار داد.

مهم ترین نکته این است که هیچ نسخه ردیفی در فهرست ها از هر نوع وجود ندارد. خوب، یا می توانیم فرض کنیم که هر خط دقیقاً با یک نسخه نشان داده شده است. به عبارت دیگر، هیچ فیلد xmin و xmax در هدر ردیف فهرست وجود ندارد. می‌توانیم فرض کنیم که پیوندهای فهرست به تمام نسخه‌های جدول ردیف‌ها منتهی می‌شوند - بنابراین می‌توانید بفهمید که تراکنش کدام نسخه را تنها با نگاه کردن به جدول خواهد دید. (مثل همیشه، این تمام حقیقت نیست. در برخی موارد، نقشه دید می تواند روند را بهینه کند، اما بعداً با جزئیات بیشتری به این موضوع خواهیم پرداخت.)

در همان زمان، در صفحه فهرست، نشانگرهای هر دو نسخه، هم نسخه فعلی و هم نسخه قدیمی را پیدا می کنیم:

=> SELECT itemoffset, ctid FROM bt_page_items('t_s_idx',1);
 itemoffset | ctid  
------------+-------
          1 | (0,2)
          2 | (0,1)
(2 rows)

معاملات مجازی

در عمل، PostgreSQL از بهینه‌سازی‌هایی استفاده می‌کند که به آن اجازه می‌دهد اعداد تراکنش‌ها را «ذخیره» کند.

اگر تراکنش فقط داده ها را بخواند، تاثیری بر رویت نسخه های ردیفی ندارد. بنابراین، فرآیند سرویس ابتدا یک xid مجازی برای تراکنش صادر می کند. این شماره شامل شناسه فرآیند و شماره توالی است.

صدور این شماره نیازی به همگام سازی بین تمام فرآیندها ندارد و بنابراین بسیار سریع است. وقتی صحبت از انجماد می شود با دلیل دیگری برای استفاده از شماره های مجازی آشنا می شویم.

اعداد مجازی به هیچ وجه در عکس های فوری داده ها در نظر گرفته نمی شوند.

در مقاطع مختلف زمانی، ممکن است تراکنش های مجازی با اعدادی که قبلاً استفاده شده اند در سیستم وجود داشته باشد و این طبیعی است. اما چنین عددی را نمی توان در صفحات داده نوشت، زیرا دفعه بعد که به صفحه دسترسی پیدا می کند ممکن است تمام معنی خود را از دست بدهد.

=> BEGIN;
=> SELECT txid_current_if_assigned();
 txid_current_if_assigned 
--------------------------
                         
(1 row)

اگر یک تراکنش شروع به تغییر داده ها کند، یک شماره تراکنش واقعی و منحصر به فرد به آن داده می شود.

=> UPDATE accounts SET amount = amount - 1.00;
=> SELECT txid_current_if_assigned();
 txid_current_if_assigned 
--------------------------
                     3667
(1 row)

=> COMMIT;

معاملات تو در تو

ذخیره امتیاز

در SQL تعریف شده است ذخیره امتیاز (savepoint)، که به شما امکان می دهد بخشی از یک تراکنش را بدون قطع کامل آن لغو کنید. اما این در نمودار بالا نمی گنجد، زیرا تراکنش برای همه تغییراتش وضعیت یکسانی دارد و از نظر فیزیکی هیچ داده ای برگشت داده نمی شود.

برای پیاده سازی این قابلیت، یک تراکنش با یک ذخیره به چندین جداگانه تقسیم می شود معاملات تو در تو (تراکنش فرعی)، که وضعیت آن را می توان به طور جداگانه مدیریت کرد.

تراکنش های تو در تو تعداد خاص خود را دارند (بیشتر از تعداد تراکنش اصلی). وضعیت تراکنش های تودرتو به روش معمول در XACT ثبت می شود، اما وضعیت نهایی به وضعیت تراکنش اصلی بستگی دارد: اگر لغو شود، تمام تراکنش های تودرتو نیز لغو می شوند.

اطلاعات مربوط به تودرتوی تراکنش ها در فایل های دایرکتوری PGDATA/pg_subtrans ذخیره می شود. فایل ها از طریق بافرهای موجود در حافظه مشترک نمونه قابل دسترسی هستند که به همان شیوه بافرهای XACT سازماندهی شده اند.

تراکنش های تو در تو را با تراکنش های مستقل اشتباه نگیرید. تراکنش های مستقل به هیچ وجه به یکدیگر وابسته نیستند، اما تراکنش های تودرتو به یکدیگر وابسته هستند. هیچ تراکنش مستقلی در PostgreSQL معمولی وجود ندارد، و شاید برای بهترین حالت: آنها بسیار بسیار بندرت مورد نیاز هستند و حضور آنها در سایر DBMS ها باعث سوء استفاده می شود، که همه از آن رنج می برند.

بیایید جدول را پاک کنیم، تراکنش را شروع کنیم و ردیف را درج کنیم:

=> TRUNCATE TABLE t;
=> BEGIN;
=> INSERT INTO t(s) VALUES ('FOO');
=> SELECT txid_current();
 txid_current 
--------------
         3669
(1 row)

=> SELECT xmin, xmax, * FROM t;
 xmin | xmax | id |  s  
------+------+----+-----
 3669 |    0 |  2 | FOO
(1 row)

=> SELECT * FROM heap_page('t',0);
 ctid  | state  | xmin | xmax  | t_ctid 
-------+--------+------+-------+--------
 (0,1) | normal | 3669 | 0 (a) | (0,1)
(1 row)

حالا یک نقطه ذخیره قرار می دهیم و یک خط دیگر وارد می کنیم.

=> SAVEPOINT sp;
=> INSERT INTO t(s) VALUES ('XYZ');
=> SELECT txid_current();
 txid_current 
--------------
         3669
(1 row)

توجه داشته باشید که تابع ()txid_current شماره تراکنش اصلی را برمی گرداند، نه شماره تراکنش تودرتو.

=> SELECT xmin, xmax, * FROM t;
 xmin | xmax | id |  s  
------+------+----+-----
 3669 |    0 |  2 | FOO
 3670 |    0 |  3 | XYZ
(2 rows)

=> SELECT * FROM heap_page('t',0);
 ctid  | state  | xmin | xmax  | t_ctid 
-------+--------+------+-------+--------
 (0,1) | normal | 3669 | 0 (a) | (0,1)
 (0,2) | normal | 3670 | 0 (a) | (0,2)
(2 rows)

بیایید به نقطه ذخیره برگردیم و خط سوم را وارد کنیم.

=> ROLLBACK TO sp;
=> INSERT INTO t(s) VALUES ('BAR');
=> SELECT xmin, xmax, * FROM t;
 xmin | xmax | id |  s  
------+------+----+-----
 3669 |    0 |  2 | FOO
 3671 |    0 |  4 | BAR
(2 rows)

=> SELECT * FROM heap_page('t',0);
 ctid  | state  |   xmin   | xmax  | t_ctid 
-------+--------+----------+-------+--------
 (0,1) | normal | 3669     | 0 (a) | (0,1)
 (0,2) | normal | 3670 (a) | 0 (a) | (0,2)
 (0,3) | normal | 3671     | 0 (a) | (0,3)
(3 rows)

در صفحه همچنان ردیف اضافه شده توسط تراکنش تودرتوی لغو شده را مشاهده می کنیم.

تغییرات را اصلاح می کنیم.

=> COMMIT;
=> SELECT xmin, xmax, * FROM t;
 xmin | xmax | id |  s  
------+------+----+-----
 3669 |    0 |  2 | FOO
 3671 |    0 |  4 | BAR
(2 rows)

=> SELECT * FROM heap_page('t',0);
 ctid  | state  |   xmin   | xmax  | t_ctid 
-------+--------+----------+-------+--------
 (0,1) | normal | 3669 (c) | 0 (a) | (0,1)
 (0,2) | normal | 3670 (a) | 0 (a) | (0,2)
 (0,3) | normal | 3671 (c) | 0 (a) | (0,3)
(3 rows)

اکنون به وضوح می توانید ببینید که هر تراکنش تودرتو وضعیت خاص خود را دارد.

توجه داشته باشید که تراکنش‌های تودرتو را نمی‌توان به طور صریح در SQL استفاده کرد، یعنی نمی‌توانید تراکنش جدیدی را بدون تکمیل تراکنش فعلی شروع کنید. این مکانیسم به طور ضمنی در هنگام استفاده از نقاط ذخیره، و همچنین هنگام مدیریت استثناهای PL/pgSQL و در تعدادی از موارد عجیب‌تر دیگر فعال می‌شود.

=> BEGIN;
BEGIN
=> BEGIN;
WARNING:  there is already a transaction in progress
BEGIN
=> COMMIT;
COMMIT
=> COMMIT;
WARNING:  there is no transaction in progress
COMMIT

خطاها و اتمی بودن عملیات

اگر هنگام انجام یک عملیات خطایی رخ دهد چه اتفاقی می افتد؟ به عنوان مثال، مانند این:

=> BEGIN;
=> SELECT * FROM t;
 id |  s  
----+-----
  2 | FOO
  4 | BAR
(2 rows)

=> UPDATE t SET s = repeat('X', 1/(id-4));
ERROR:  division by zero

خطایی رخ داده است. اکنون تراکنش لغو شده در نظر گرفته می شود و هیچ عملیاتی در آن مجاز نیست:

=> SELECT * FROM t;
ERROR:  current transaction is aborted, commands ignored until end of transaction block

و حتی اگر بخواهید تغییرات را انجام دهید، PostgreSQL یک سقط را گزارش می کند:

=> COMMIT;
ROLLBACK

چرا یک تراکنش نمی تواند پس از شکست ادامه یابد؟ واقعیت این است که خطا می تواند به گونه ای رخ دهد که ما به بخشی از تغییرات دسترسی پیدا کنیم - اتمی بودن حتی تراکنش نیست، بلکه اپراتور نقض می شود. مانند مثال ما، جایی که اپراتور موفق شد قبل از خطا یک خط را به روز کند:

=> SELECT * FROM heap_page('t',0);
 ctid  | state  |   xmin   | xmax  | t_ctid 
-------+--------+----------+-------+--------
 (0,1) | normal | 3669 (c) | 3672  | (0,4)
 (0,2) | normal | 3670 (a) | 0 (a) | (0,2)
 (0,3) | normal | 3671 (c) | 0 (a) | (0,3)
 (0,4) | normal | 3672     | 0 (a) | (0,4)
(4 rows)

باید گفت که psql حالتی دارد که همچنان به تراکنش اجازه می دهد تا پس از شکست به گونه ای ادامه یابد که گویی اقدامات اپراتور اشتباه به عقب برگشته است.

=> set ON_ERROR_ROLLBACK on
=> BEGIN;
=> SELECT * FROM t;
 id |  s  
----+-----
  2 | FOO
  4 | BAR
(2 rows)

=> UPDATE t SET s = repeat('X', 1/(id-4));
ERROR:  division by zero

=> SELECT * FROM t;
 id |  s  
----+-----
  2 | FOO
  4 | BAR
(2 rows)

=> COMMIT;

حدس زدن اینکه در این حالت، psql در واقع قبل از هر دستور یک ذخیره ضمنی قرار می دهد و در صورت خرابی شروع به بازگشت به آن می کند، دشوار نیست. این حالت به‌طور پیش‌فرض استفاده نمی‌شود، زیرا تنظیم نقاط ذخیره (حتی بدون بازگشت به آنها) مستلزم سربار قابل توجهی است.

ادامه

منبع: www.habr.com

اضافه کردن نظر