0

PostgreSQL Concurrency Control (P1): Cơ chế MVCC và kiến trúc Tuple

Chào mọi người! Trong bài viết này, chúng ta cùng tìm hiểu về cách Postgres quản lý concurrency.

Quản lý concurrency là cơ chế đảm bảo tính I (Isolation hay tính cô lập) trong ACID khi có nhiều transaction chạy cùng lúc. PostgreSQL sử dụng kỹ thuật MVCC (Multi-Version Concurrency Control hay Kiểm soát đồng thời đa phiên bản), được triển khai theo mô hình Snapshot Isolation (SI).

Trong phần 1 này, chúng ta sẽ cùng tìm hiểu về:

  • Transaction ID
  • Cấu trúc của một bản ghi (Tuple)
  • Cơ chế INSERT, DELETE, UPDATE
  • Transaction Snapshot

Trong bài viết sẽ sử dụng bảng tbl làm ví dụ.

CREATE TABLE tbl (
   id SERIAL,
   data VARCHAR(255) NOT NULL
);

CREATE EXTENSION IF NOT EXISTS pageinspect;

1. Transaction ID

Khi bắt đầu 1 transaction, transaction manager sẽ tạo ra 1 ID duy nhất, gọi là txid (transaction id). Postgres cung cấp hàm pg_current_xact_id để truy vấn txid của transaction.

-- Bắt đầu transaction
BEGIN;

SELECT pg_current_xact_id();
/*
Kết quả: 
+--------------+
| txid_current |
|--------------|
| 100          |
+--------------+
*/

COMMIT;

2. Cấu trúc của bản ghi (Tuple)

Một bản ghi tiêu chuẩn gồm 3 thành phần chính: HeapTupleHeaderData, Null bitmap, và User data.

Minh họa visible/invisible của txid
Hình 1.2: Cấu trúc của một tuple trong Postgres

Trong phần HeapTupleHeaderData, có 4 trường thông tin đặc biệt quan trọng giúp Postgres xác định được bản ghi hợp lệ đối với mỗi Transaction:

  • t_xmin: Lưu trữ txid của transaction đã INSERT bản ghi này.
  • t_xmax: Lưu trữ txid của transaction đã DELETE hoặc UPDATE bản ghi này. (Nếu bản ghi chưa bị DELETE hoặc UPDATE, giá trị này bằng 0).
  • t_ctid: Lưu trữ tuple ID (TID), trỏ đến tuple của nó hoặc bản ghi mới.
    • Nếu bản ghi chưa bị UPDATE, t_ctid trỏ về chính vị trí của nó.
    • Khi bản ghi được UPDATE, Postgres sẽ chèn một bản ghi mới vào bảng và cập nhật t_ctid của bản ghi cũ trỏ trực tiếp đến vị trí vật lý của bản ghi mới đó.

Việc biết được txid nào thực hiện insert hay update bản ghi, giúp Postgres xác định được phiên bản nào là hợp lệ thông qua visible rule check.

3. INSERT, DELETE và UPDATE dưới góc độ MVCC

3.1 INSERT

Giả sử thực hiện câu lệnh INSERT trong một transaction có txid = 99. Bản ghi mới sẽ được tạo như sau: Minh họa trạng thái tuple sau khi INSERT Trong đó:

  • t_xmin: Gán bằng 99 (txid của transaction thực hiện lệnh INSERT).
  • t_xmax: Gán bằng 0, vì tuple này chưa bị DELETE hay UPDATE.
  • t_cid: Gán bằng 0, cho biết đây là câu lệnh SQL đầu tiên được thực thi bởi transaction 99.
  • t_ctid: Gán bằng (0, 1). Nó trỏ về chính nó vì đây đang là phiên bản mới nhất (và duy nhất) của bản ghi.

Test trong thực tế:

BEGIN;
SELECT pg_current_xact_id(); -- Giả sử trả về txid: 99

INSERT INTO tbl (data) VALUES ('A');


SELECT lp as tuple, t_xmin, t_xmax, t_field3 as t_cid, t_ctid
    FROM heap_page_items(get_raw_page('tbl', 0));
/*
 xmin | cmin | xmax | cmax | ctid  | id | data 
------+------+------+------+-------+----+------
   99 |    0 |    0 |    0 | (0,1) |  1 | A    
*/

COMMIT;

3.2. DELETE

Khi thực hiện lệnh DELETE, bản ghi không bị xóa ngay lập tức khỏi đĩa cứng mà chỉ bị xóa về mặt logic (soft delete). Giả sử transaction txid = 100 thực hiện xóa Tuple_1:

  • t_xmax: Được gán bằng 100.

Khi transaction 100 COMMIT, Tuple_1 chính thức trở thành một dead tuple (bản ghi rác). Lúc này, không một transaction nào trong tương lai có thể nhìn thấy nó nữa, và tiến trình VACCUM sẽ dọn dẹp bản ghi này.

image.png

3.3. UPDATE

Khi thực hiện UPDATE, Postgres không sửa trực tiếp dữ liệu cũ, mà thực hiện xóa logic tuple cũ và INSERT một tuple mới.

Giả sử một dòng dữ liệu được INSERT ban đầu bởi txid = 99, sau đó được UPDATE 2 lần liên tiếp trong cùng một transaction txid = 100:

Minh họa visible/invisible của txid
Hình 1.5:

Lần UPDATE thứ nhất: Postgres xóa logic Tuple_1 bằng cách set t_xmax của nó thành 100, đồng thời insert Tuple_2 mới.

  • Tuple_1:
    • t_xmax: được set thành 100.
    • t_ctid: đổi từ (0, 1) thành (0, 2).
  • Tuple_2:
    • t_xmin: được set thành 100.
    • t_xmax: được set thành 0.
    • t_cid: được set thành 0.
    • t_ctid: được set thành (0, 2).

Lần UPDATE thứ hai: tương tự, Tuple_2 bị xóa logic, và Tuple_3 được insert.

  • Tuple_2:
    • t_xmax: được set thành 100.
    • t_ctid: đổi từ (0, 2) thành (0, 3).
  • Tuple_3:
    • t_xmin: được set thành 100.
    • t_xmax: được set thành 0.
    • t_cid: được set thành 1.
    • t_ctid: được set thành (0, 3).

Nếu transaction 100 COMMIT, Tuple_1 và Tuple_2 trở thành Dead Tuple. Ngược lại, nếu txid 100 ABORT, Tuple_2Tuple_3 trở thành Dead Tuple.

Câu lệnh test thực tế

BEGIN;
-- Txid: For example: 200
SELECT pg_current_xact_id();

INSERT INTO tbl (data) VALUES ('A');
UPDATE tbl SET data = 'B' WHERE data = 'A';
UPDATE tbl SET data = 'C' WHERE data = 'B';
COMMIT;

-- Kiểm tra thông tin của bản ghi. 
-- Lưu ý: Cần cài đặt thêm extension: CREATE EXTENSION IF NOT EXISTS pageinspect;
SELECT lp as tuple, t_xmin, t_xmax, t_field3 as t_cid, t_ctid
FROM heap_page_items(get_raw_page('tbl', 0));

4. Transaction Snapshot

Transaction Snapshots là nơi lưu trữ tất cả các transaction đang hoạt động tại 1 thời điểm nhất định. Postgre cung cấp hàm pg_current_snapshot để xem được snapshot:

SELECT pg_current_snapshot();

-- pg_current_snapshot
-- -------------------
-- 100:104:100,102
-- (1 row)

Cấu trúc của 1 transaction snapshot: xmin:xmax:xip_list, trong đó:

  • xmin: Transaction sớm nhất vẫn còn hoạt động. Những transaction trước đó có thể hoặc commited hoặc rolled back.
  • xmax: Transaction kế tiếp sẽ được cấp phát, chưa hề tồn tại tại thời điểm snapshot.
  • xip_list: Danh sách các txid đang hoạt động.

image.png

Transaction Manager cung cấp những snapshot này cho các transaction dựa trên isolation level.

  • Đối với READ COMMITED, transaction sẽ được cập nhật mỗi khi câu SQL được thực thi.
  • Ngược lại, REPEATABLE READ or SERIALIZABLE, transaction snapshot chỉ được cung cấp 1 lần duy nhất (khi câu SQL đầu tiên được thực thi)

Trong ví dụ dưới đây, 3 transaction lần lượt thực thi: Transaction A và Transaction_B sử dụng READ COMMITED, TRANSACTION_C sử dụng REPEATABLE READ.

image.png

  • T1: Transaction_A (txid = 200) chạy và thực hiện câu lệnh SELECT. Tại thời điểm này, transaction manager sẽ trả về snapshot '200:200'
  • T2: Transaction_B (txid=201) khởi tạo và thực hiện câu lệnh SELECT. Tại thời điểm này, transaction manager trả về snapshot '200:200'.
  • T3: Transaction_C (txid = 202) khởi tạo và thực hiện câu lệnh SELECT. Tại thời điểm này, transaction manager trả về snapshot (200:200). Đây cũng là phiên bản snapshot duy nhất của Transaction_C
  • T4: Transaction_A commit.
  • T5: Transaction_B thực hiện câu lệnh SELECT, lúc này, snap được cập nhật lại là snapshot="201:201:". Transaction_C thực hiện câu lệnh SELECT, nhưng không được cập nhật snapshot mới.

Tạm kết

Hiểu rõ Transaction ID, cấu trúc Tuple Header và cách INSERT / UPDATE / DELETE vận hành chính là bản chất để bạn hiểu rõ cơ chế quản lý concurrency trong Postgres.

Ở Phần 2, chúng ta sẽ cùng đi sâu vào phân tích các hiện tượng xung đột dữ liệu (Read & Write Phenomena) khi các transaction chạy đồng thời.

Cảm ơn các bạn đã dành thời gian theo dõi bài viết.

Nguồn tham khảo:


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí