MVCC-3. Phiên bản chuỗi

Vì vậy, chúng tôi đã xem xét các vấn đề liên quan đến vật liệu cách nhiệt, và rút lui về tổ chức dữ liệu ở mức độ thấp. Và cuối cùng chúng ta đã đến phần thú vị nhất - phiên bản chuỗi.

Tiêu đề

Như chúng tôi đã nói, mỗi hàng có thể tồn tại đồng thời ở nhiều phiên bản trong cơ sở dữ liệu. Phiên bản này phải được phân biệt bằng cách nào đó với phiên bản khác, vì mục đích này, mỗi phiên bản có hai dấu xác định “thời gian” hoạt động của phiên bản này (xmin và xmax). Trong dấu ngoặc kép - bởi vì không phải thời gian được sử dụng mà là một bộ đếm tăng đặc biệt. Và bộ đếm này chính là số giao dịch.

(Như thường lệ, thực tế phức tạp hơn: số lượng giao dịch không thể tăng liên tục do dung lượng bit của bộ đếm có hạn. Nhưng chúng ta sẽ xem xét các chi tiết này một cách chi tiết khi chúng ta bắt đầu đóng băng.)

Khi một hàng được tạo, xmin được đặt thành số giao dịch đã đưa ra lệnh INSERT và xmax được để trống.

Khi một hàng bị xóa, giá trị xmax của phiên bản hiện tại được đánh dấu bằng số lượng giao dịch đã thực hiện XÓA.

Khi một hàng được sửa đổi bằng lệnh UPDATE, hai thao tác thực sự được thực hiện: DELETE và INSERT. Phiên bản hiện tại của hàng đặt xmax bằng số lượng giao dịch đã thực hiện CẬP NHẬT. Sau đó, một phiên bản mới của cùng một chuỗi sẽ được tạo; giá trị xmin của nó trùng với giá trị xmax của phiên bản trước.

Các trường xmin và xmax được bao gồm trong tiêu đề phiên bản hàng. Ngoài các trường này, tiêu đề còn chứa các trường khác, ví dụ:

  • infomask là một chuỗi bit xác định các thuộc tính của phiên bản này. Có khá nhiều trong số họ; Chúng tôi sẽ dần dần xem xét những cái chính.
  • ctid là liên kết tới phiên bản tiếp theo, mới hơn của cùng dòng. Đối với phiên bản mới nhất, mới nhất của chuỗi, ctid đề cập đến chính phiên bản này. Số có dạng (x,y), trong đó x là số trang, y là số chỉ mục trong mảng.
  • bitmap null - Đánh dấu các cột của một phiên bản nhất định có chứa giá trị null (NULL). NULL không phải là một trong những giá trị kiểu dữ liệu thông thường, do đó thuộc tính này phải được lưu trữ riêng.

Kết quả là tiêu đề khá lớn - ít nhất 23 byte cho mỗi phiên bản của dòng và thường nhiều hơn do bitmap NULL. Nếu bảng "hẹp" (nghĩa là chứa ít cột), chi phí có thể chiếm nhiều hơn thông tin hữu ích.

chèn

Chúng ta hãy xem xét kỹ hơn cách thực hiện các thao tác chuỗi cấp thấp, bắt đầu bằng việc chèn.

Đối với các thử nghiệm, hãy tạo một bảng mới có hai cột và chỉ mục trên một trong các cột đó:

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

Hãy chèn một hàng sau khi bắt đầu giao dịch.

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

Đây là số giao dịch hiện tại của chúng tôi:

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

Chúng ta hãy nhìn vào nội dung của trang. Hàm heap_page_items của tiện ích mở rộng pageinspect cho phép bạn lấy thông tin về con trỏ và phiên bản hàng:

=> 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

Lưu ý rằng đống từ trong PostgreSQL đề cập đến các bảng. Đây là một cách sử dụng kỳ lạ khác của thuật ngữ - một đống được biết đến cấu trúc dữ liệu, không có gì chung với bảng. Ở đây từ này được dùng theo nghĩa “mọi thứ được kết hợp lại với nhau”, trái ngược với các chỉ mục có thứ tự.

Hàm này hiển thị dữ liệu “nguyên trạng”, ở định dạng khó hiểu. Để tìm ra điều đó, chúng tôi sẽ chỉ để lại một phần thông tin và giải mã nó:

=> 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)

Đây là những gì chúng tôi đã làm:

  • Đã thêm số XNUMX vào số chỉ mục để làm cho nó trông giống như t_ctid: (số trang, số chỉ mục).
  • Đã giải mã trạng thái của con trỏ lp_flags. Ở đây là "bình thường" - điều này có nghĩa là con trỏ thực sự đề cập đến phiên bản của chuỗi. Chúng ta sẽ xem xét các ý nghĩa khác sau.
  • Trong số tất cả các bit thông tin, cho đến nay chỉ có hai cặp được xác định. Các bit xmin_commned và xmin_aborted cho biết số giao dịch xmin có được cam kết hay không (bị hủy bỏ). Hai bit giống nhau đề cập đến số giao dịch xmax.

Chúng ta thấy gì? Khi bạn chèn một hàng, chỉ mục số 1 sẽ xuất hiện trong trang bảng, trỏ đến phiên bản đầu tiên và duy nhất của hàng.

Trong phiên bản chuỗi, trường xmin chứa số giao dịch hiện tại. Giao dịch vẫn đang hoạt động nên cả hai bit xmin_commit và xmin_aborted đều không được đặt.

Trường ctid của phiên bản hàng đề cập đến cùng một hàng. Điều này có nghĩa là phiên bản mới hơn không tồn tại.

Trường xmax được điền bằng số giả 0 vì phiên bản của hàng này chưa bị xóa và vẫn còn cập nhật. Các giao dịch sẽ không chú ý đến con số này vì bit xmax_aborted đã được đặt.

Hãy thực hiện thêm một bước nữa để cải thiện khả năng đọc bằng cách thêm các bit thông tin vào số giao dịch. Và hãy tạo một hàm vì chúng ta sẽ cần yêu cầu này nhiều lần:

=> 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;

Ở dạng này, những gì đang diễn ra trong tiêu đề của phiên bản hàng sẽ rõ ràng hơn nhiều:

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

Tương tự, nhưng ít chi tiết hơn đáng kể, thông tin có thể được lấy từ chính bảng đó bằng cách sử dụng các cột giả xmin và xmax:

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

Cố định

Nếu một giao dịch được hoàn thành thành công, bạn cần nhớ trạng thái của nó - lưu ý rằng nó đã được cam kết. Để thực hiện điều này, một cấu trúc có tên XACT được sử dụng (và trước phiên bản 10, nó được gọi là CLOG (nhật ký cam kết) và tên này vẫn có thể được tìm thấy ở những nơi khác nhau).

XACT không phải là bảng danh mục hệ thống; đây là các tệp trong thư mục PGDATA/pg_xact. Chúng có hai bit cho mỗi giao dịch: đã cam kết và bị hủy bỏ - giống như trong tiêu đề phiên bản hàng. Thông tin này được chia thành nhiều tệp chỉ để thuận tiện; chúng tôi sẽ quay lại vấn đề này khi xem xét việc đóng băng. Và công việc với những tệp này được thực hiện theo từng trang, giống như với tất cả những tệp khác.

Vì vậy, khi một giao dịch được thực hiện trong XACT, bit đã cam kết sẽ được đặt cho giao dịch này. Và đây là tất cả những gì xảy ra trong quá trình cam kết (mặc dù chúng ta chưa nói về nhật ký ghi trước).

Khi một giao dịch khác truy cập vào trang bảng mà chúng ta vừa xem, nó sẽ phải trả lời một số câu hỏi.

  1. Giao dịch xmin đã hoàn tất chưa? Nếu không thì phiên bản đã tạo của chuỗi sẽ không hiển thị.
    Việc kiểm tra này được thực hiện bằng cách xem xét một cấu trúc khác nằm trong bộ nhớ dùng chung của phiên bản và được gọi là ProcArray. Nó chứa danh sách tất cả các quy trình đang hoạt động và đối với mỗi quy trình, số lượng giao dịch hiện tại (đang hoạt động) của nó được chỉ định.
  2. Nếu hoàn thành thì làm thế nào - bằng cách cam kết hoặc hủy bỏ? Nếu bị hủy thì phiên bản hàng cũng sẽ không hiển thị.
    Đây chính xác là mục đích của XACT. Tuy nhiên, mặc dù các trang cuối cùng của XACT được lưu trữ trong bộ đệm trong RAM nhưng việc kiểm tra XACT mỗi lần vẫn rất tốn kém. Do đó, khi trạng thái giao dịch được xác định, nó sẽ được ghi vào các bit xmin_commit và xmin_aborted của phiên bản chuỗi. Nếu một trong các bit này được đặt thì trạng thái giao dịch xmin được coi là đã biết và giao dịch tiếp theo sẽ không phải truy cập XACT.

Tại sao các bit này không được thiết lập bởi chính giao dịch khi thực hiện thao tác chèn? Khi việc chèn xảy ra, giao dịch vẫn chưa biết liệu nó có thành công hay không. Và đến lúc cam kết thì không còn rõ dòng nào bị thay đổi ở trang nào nữa. Có thể có rất nhiều trang như vậy và việc ghi nhớ chúng là không có lợi. Ngoài ra, một số trang có thể bị xóa khỏi bộ đệm đệm vào đĩa; đọc lại chúng để thay đổi các bit sẽ làm chậm đáng kể quá trình cam kết.

Nhược điểm của việc tiết kiệm là sau khi thay đổi, bất kỳ giao dịch nào (ngay cả một giao dịch thực hiện thao tác đọc đơn giản - CHỌN) đều có thể bắt đầu thay đổi các trang dữ liệu trong bộ đệm đệm.

Vì vậy, hãy sửa chữa sự thay đổi.

=> COMMIT;

Không có gì thay đổi trên trang (nhưng chúng tôi biết rằng trạng thái giao dịch đã được ghi lại trong XACT):

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

Bây giờ giao dịch truy cập trang đầu tiên sẽ phải xác định trạng thái giao dịch xmin và ghi nó vào các bit thông tin:

=> 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)

Xóa

Khi một hàng bị xóa, số giao dịch xóa hiện tại được ghi vào trường xmax của phiên bản hiện tại và bit xmax_aborted sẽ bị xóa.

Lưu ý rằng giá trị được đặt của xmax tương ứng với giao dịch đang hoạt động đóng vai trò khóa hàng. Nếu một giao dịch khác muốn cập nhật hoặc xóa hàng này, nó sẽ buộc phải đợi giao dịch xmax hoàn tất. Chúng ta sẽ nói nhiều hơn về việc chặn sau. Hiện tại, chúng tôi chỉ lưu ý rằng số lượng khóa hàng là không giới hạn. Chúng không chiếm dung lượng RAM và hiệu suất hệ thống không bị ảnh hưởng bởi số lượng của chúng. Đúng là các giao dịch “dài” có những nhược điểm khác, nhưng sau này chúng ta sẽ nói nhiều hơn về điều đó.

Hãy xóa dòng này.

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

Chúng tôi thấy rằng số giao dịch được ghi trong trường xmax, nhưng các bit thông tin không được đặt:

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

Hủy bỏ

Việc hủy bỏ các thay đổi hoạt động tương tự như cam kết, chỉ trong XACT bit bị hủy bỏ được đặt cho giao dịch. Việc hoàn tác cũng nhanh như cam kết. Mặc dù lệnh được gọi là ROLLBACK nhưng các thay đổi sẽ không được khôi phục: mọi thứ mà giao dịch được quản lý để thay đổi trong các trang dữ liệu vẫn không thay đổi.

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

Khi trang được truy cập, trạng thái sẽ được kiểm tra và bit gợi ý xmax_aborted sẽ được đặt thành phiên bản hàng. Bản thân số xmax vẫn còn trên trang nhưng sẽ không có ai nhìn vào nó.

=> 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)

Cập nhật

Bản cập nhật hoạt động như thể lần đầu tiên nó xóa phiên bản hiện tại của hàng rồi chèn một phiên bản mới.

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

Truy vấn tạo ra một dòng (phiên bản mới):

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

Nhưng trên trang chúng tôi thấy cả hai phiên bản:

=> 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)

Phiên bản đã xóa được đánh dấu bằng số giao dịch hiện tại trong trường xmax. Hơn nữa, giá trị này được ghi đè lên giá trị cũ vì giao dịch trước đó đã bị hủy. Và bit xmax_aborted bị xóa vì chưa biết trạng thái của giao dịch hiện tại.

Phiên bản đầu tiên của dòng hiện đề cập đến phiên bản thứ hai (trường t_ctid) là phiên bản mới hơn.

Chỉ mục thứ hai xuất hiện trong trang chỉ mục và hàng thứ hai tham chiếu đến phiên bản thứ hai trong trang bảng.

Cũng giống như việc xóa, giá trị xmax trong phiên bản đầu tiên của hàng là dấu hiệu cho thấy hàng đó đã bị khóa.

Được rồi, hãy hoàn tất giao dịch.

=> COMMIT;

Chỉ số

Cho đến nay chúng ta chỉ nói về các trang bảng. Điều gì xảy ra bên trong các chỉ mục?

Thông tin trong các trang chỉ mục rất khác nhau tùy thuộc vào loại chỉ mục cụ thể. Và thậm chí một loại chỉ mục có nhiều loại trang khác nhau. Ví dụ: cây B có trang siêu dữ liệu và các trang “thông thường”.

Tuy nhiên, trang thường có một mảng các con trỏ tới các hàng và chính các hàng đó (giống như một trang bảng). Ngoài ra, ở cuối trang có chỗ dành cho dữ liệu đặc biệt.

Các hàng trong chỉ mục cũng có thể có cấu trúc rất khác nhau tùy thuộc vào loại chỉ mục. Ví dụ: đối với cây B, các hàng liên quan đến các trang lá chứa giá trị khóa lập chỉ mục và một tham chiếu (ctid) cho hàng tương ứng của bảng. Nói chung, chỉ mục có thể được cấu trúc theo một cách hoàn toàn khác.

Điểm quan trọng nhất là không có phiên bản hàng nào trong bất kỳ loại chỉ mục nào. Chà, hoặc chúng ta có thể giả định rằng mỗi dòng được thể hiện bằng chính xác một phiên bản. Nói cách khác, không có trường xmin và xmax trong tiêu đề hàng chỉ mục. Chúng ta có thể giả định rằng các liên kết từ chỉ mục dẫn đến tất cả các phiên bản bảng của các hàng - vì vậy bạn có thể biết giao dịch sẽ thấy phiên bản nào chỉ bằng cách nhìn vào bảng. (Như mọi khi, đây không phải là toàn bộ sự thật. Trong một số trường hợp, bản đồ hiển thị có thể tối ưu hóa quy trình, nhưng chúng ta sẽ xem xét vấn đề này chi tiết hơn sau.)

Đồng thời, trong trang chỉ mục, chúng tôi tìm thấy các con trỏ tới cả hai phiên bản, cả phiên bản hiện tại và phiên bản cũ:

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

Giao dịch ảo

Trong thực tế, PostgreSQL sử dụng các tính năng tối ưu hóa cho phép nó “lưu” số lượng giao dịch.

Nếu một giao dịch chỉ đọc dữ liệu thì nó không ảnh hưởng đến khả năng hiển thị của các phiên bản hàng. Do đó, quy trình dịch vụ trước tiên sẽ đưa ra một xid ảo cho giao dịch. Số này bao gồm ID tiến trình và số thứ tự.

Việc cấp số này không yêu cầu đồng bộ hóa giữa tất cả các quy trình và do đó rất nhanh. Chúng ta sẽ làm quen với một lý do khác để sử dụng số ảo khi nói về việc đóng băng.

Các số ảo không được tính đến dưới bất kỳ hình thức nào trong ảnh chụp nhanh dữ liệu.

Tại các thời điểm khác nhau, có thể có các giao dịch ảo trong hệ thống với các số đã được sử dụng và điều này là bình thường. Nhưng con số như vậy không thể ghi vào trang dữ liệu, vì lần truy cập tiếp theo trang đó có thể mất hết ý nghĩa.

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

Nếu một giao dịch bắt đầu thay đổi dữ liệu, nó sẽ được cấp một số giao dịch thực, duy nhất.

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

=> COMMIT;

Giao dịch lồng nhau

Lưu điểm

Được xác định trong SQL lưu điểm (savepoint), cho phép bạn hủy một phần giao dịch mà không làm gián đoạn hoàn toàn. Nhưng điều này không phù hợp với sơ đồ trên, vì giao dịch có cùng trạng thái cho tất cả các thay đổi của nó và về mặt vật lý không có dữ liệu nào được khôi phục.

Để thực hiện chức năng này, một giao dịch có điểm lưu trữ được chia thành nhiều giao dịch riêng biệt. giao dịch lồng nhau (giao dịch phụ), trạng thái của nó có thể được quản lý riêng.

Các giao dịch lồng nhau có số riêng (cao hơn số lượng giao dịch chính). Trạng thái của các giao dịch lồng nhau được ghi lại theo cách thông thường trong XACT, nhưng trạng thái cuối cùng phụ thuộc vào trạng thái của giao dịch chính: nếu nó bị hủy thì tất cả các giao dịch lồng nhau cũng bị hủy.

Thông tin về lồng giao dịch được lưu trữ trong các tệp trong thư mục PGDATA/pg_subtrans. Các tệp được truy cập thông qua bộ đệm trong bộ nhớ dùng chung của phiên bản, được tổ chức giống như bộ đệm XACT.

Đừng nhầm lẫn các giao dịch lồng nhau với các giao dịch tự trị. Các giao dịch tự trị không phụ thuộc vào nhau theo bất kỳ cách nào, nhưng các giao dịch lồng nhau thì có. Không có giao dịch tự trị nào trong PostgreSQL thông thường và có lẽ là điều tốt nhất: chúng rất hiếm khi cần đến và sự hiện diện của chúng trong các DBMS khác sẽ gây ra sự lạm dụng, từ đó mọi người đều phải gánh chịu.

Hãy xóa bảng, bắt đầu giao dịch và chèn hàng:

=> 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)

Bây giờ hãy đặt một điểm lưu và chèn một dòng khác.

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

Lưu ý rằng hàm txid_current() trả về số giao dịch chính chứ không phải số giao dịch lồng nhau.

=> 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)

Hãy quay lại điểm lưu và chèn dòng thứ ba.

=> 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)

Trong trang, chúng ta tiếp tục thấy hàng được thêm bởi giao dịch lồng nhau đã bị hủy.

Chúng tôi sửa chữa những thay đổi.

=> 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)

Bây giờ bạn có thể thấy rõ rằng mỗi giao dịch lồng nhau đều có trạng thái riêng.

Lưu ý rằng các giao dịch lồng nhau không thể được sử dụng một cách rõ ràng trong SQL, nghĩa là bạn không thể bắt đầu một giao dịch mới mà không hoàn thành giao dịch hiện tại. Cơ chế này được kích hoạt ngầm khi sử dụng các điểm lưu trữ, cũng như khi xử lý các ngoại lệ PL/pgSQL và trong một số trường hợp khác lạ hơn.

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

Lỗi và tính nguyên tử của hoạt động

Điều gì xảy ra nếu xảy ra lỗi trong khi thực hiện một thao tác? Ví dụ như thế này:

=> 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

Một lỗi đã xảy ra. Bây giờ giao dịch được coi là bị hủy bỏ và không có hoạt động nào được phép thực hiện trong đó:

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

Và ngay cả khi bạn cố gắng thực hiện các thay đổi, PostgreSQL sẽ báo cáo việc hủy bỏ:

=> COMMIT;
ROLLBACK

Tại sao giao dịch không thể tiếp tục sau khi thất bại? Thực tế là một lỗi có thể phát sinh theo cách mà chúng tôi có thể truy cập vào một phần của các thay đổi - tính nguyên tử của thậm chí không phải giao dịch mà là người điều hành sẽ bị vi phạm. Như trong ví dụ của chúng tôi, người vận hành đã cập nhật một dòng trước lỗi:

=> 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)

Phải nói rằng psql có một chế độ vẫn cho phép giao dịch tiếp tục sau khi thất bại như thể hành động của người vận hành sai sót được khôi phục.

=> 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;

Không khó để đoán rằng trong chế độ này, psql thực sự đặt một điểm lưu ngầm trước mỗi lệnh và trong trường hợp thất bại sẽ bắt đầu quay lại điểm lưu đó. Chế độ này không được sử dụng theo mặc định vì việc thiết lập các điểm lưu trữ (ngay cả khi không quay lại chúng) đòi hỏi chi phí đáng kể.

Sự tiếp tục.

Nguồn: www.habr.com

Thêm một lời nhận xét