Проверка видимости в PostgreSQL
В данном разделе дается описание того, как PostgreSQL выполняет проверку видимости, которая представляет собой процесс выбора кортежей соответствующих версий в рамках данной транзакции. В этом разделе также описывается, как PostgreSQL предотвращает аномалии, определенные в стандарте ANSI SQL-92, а именно грязное чтение, повторяющееся чтение и фантомные чтение.
Проверка видимости
Рисунок, представленный ниже, отображает процесс проверки видимости.
Команды SQL, отображенные на рисунке выше, выполняются в следующей последовательности:
- T1: Начало транзакции (txid 200)
- T2: Начало транзакции (txid 201)
- T3 Выполнение команд SELECT txid 200 и 201
- T4: Выполнение команды UPDATE txid 200
- T5: Выполнение команд SELECT txid 200 и 201
- T6: Выполнение txid 200
- T7: Выполнение команды SELECT txid 201
Предположим, что существует только две транзакции, т. е. txid 200 и 201. Уровень изоляции txid 200 - READ COMMITTED, уровень изоляции txid 201 - либо READ COMMITTED, либо REPEATABLE READ.
Посмотрим на то, как команды SELECT выполняют проверку видимости для каждого кортежа.
Команды SELECT T3:
В T3 в таблице 'tbl' есть только Tuple_1, и согласно правилу 6 он является видимым. Поэтому команды SELECT в обеих транзакциях возвращают 'Jekyll'.
- Rule6(Tuple_1) ⇒⇒ Status(t_xmin:199) = COMMITTED ∧∧ t_xmax = INVALID ⇒⇒ Visible
testdb=# -- txid 200 testdb=# SELECT * FROM tbl; name -------- Jekyll (1 row) testdb=# -- txid 201 testdb=# SELECT * FROM tbl; name -------- Jekyll (1 row)
Команды SELECT T5:
Для начала обратим внимание на команду SELECT, выполняемую txid 200. Согласно правилу 7 Tuple_1 является невидимым, а Nuple_2 (по правилу 2) - видимым. Поэтому команда SELECT возвращает 'Hyde'.
- Rule7(Tuple_1): Status(t_xmin:199) = COMMITTED ∧∧ Status(t_xmax:200) = IN_PROGRESS ∧∧ t_xmax:200 = current_txid:200 ⇒⇒ Invisible
- Rule2(Tuple_2): Status(t_xmin:200) = IN_PROGRESS ∧∧ t_xmin:200 = current_txid:200 ∧∧ t_xmax = INVAILD ⇒⇒ Visible
testdb=# -- txid 200 testdb=# SELECT * FROM tbl; name ------
Hyde (1 row)
С другой стороны, в команде SELECT, выполняемой txid 201, Tuple_1 является видимым по правилу 8, а Tuple_2 - невидимым согласно правилу 4. Поэтому команда SELECT возвращает 'Jekyll'.
- Rule8(Tuple_1): Status(t_xmin:199) = COMMITTED ∧∧ Status(t_xmax:200) = IN_PROGRESS ∧∧ t_xmax:200 ≠≠ current_txid:201 ⇒⇒ Visible
- Rule4(Tuple_2): Status(t_xmin:200) = IN_PROGRESS ∧∧ t_xmin:200 ≠≠ current_txid:201 ⇒⇒ Invisible
testdb=# -- txid 201 testdb=# SELECT * FROM tbl; name --------
Jekyll (1 row)
Если обновленные кортежи видны из других транзакций до их фиксации, то это называется грязным чтением, также известным как wr-conflicts.
Команда SELECT T7:
Ниже дается описание поведения команд SELECT в T7 на обоих уровнях изоляции.
Когда txid 201 находится на уровне READ COMMITTED, txid 200 рассматривается как COMMITTED, поскольку моментальный снимок транзакции - '201:201:'. Таким образом, TuPle_1 являетя невидимым по правилу 10, а Tuple_2 - видимым по правилу 6. Команда SELECT возвращает 'Hyde'.
- Rule10(Tuple_1): Status(t_xmin:199) = COMMITTED ∧∧ Status(t_xmax:200) = COMMITTED ∧∧ Snapshot(t_xmax:200) ≠≠ active ⇒⇒ Invisible
- Rule6(Tuple_2): Status(t_xmin:200) = COMMITTED ∧∧ t_xmax = INVALID ⇒⇒ Visible
testdb=# -- txid 201 (READ COMMITTED) testdb=# SELECT * FROM tbl; name ------
Hyde (1 row)
Обратите внимание, что результаты команд SELECT, которые выполняются до и после фиксации txid 200, отличаются. Это является наглядным примером неповторяемоего чтения.
И наоборот, когда txid 201 находится на уровне REPEATABLE READ, txid 200 рассматривается как IN_PROGRESS, поскольку моментальный снимок транзакции - '200:200:'. Таким образом, Tuple_1 является видимым по правилу 9, а Tuple_2 - невидимым по правилу 5. Команда SELECT возвращает 'Jekyll'.
Обратите внимание на то, что неповторяемое чтение невозможно при уровне изоляции REPEATABLE READ (и SERIALIZABLE.
- Rule9(Tuple_1): Status(t_xmin:199) = COMMITTED ∧∧ Status(t_xmax:200) = COMMITTED ∧∧ Snapshot(t_xmax:200) = active ⇒⇒ Visible
- Rule5(Tuple_2): Status(t_xmin:200) = COMMITTED ∧∧ Snapshot(t_xmin:200) = active ⇒⇒ Invisible
testdb=# -- txid 201 (REPEATABLE READ) testdb=# SELECT * FROM tbl; name -------- Jekyll (1 row)
Примечание: Hint Bits
Для получения информации о состоянии транзакции PostgreSQL предоставляет три функции: TransactionIdIsInProgress, TransactionIdDidCommit и TransactionIdDidAbort. Эти функции реализованы для того, чтобы уменьшить частое обращение к CLOG, например, к кэшам. Однако если они будут выполняться каждый раз, когда проверяется каждый кортеж, возникнет bottleneck.
Для решения этой проблемы PostgreSQL использует Hint Bits:
#define HEAP_XMIN_COMMITTED 0x0100 /* t_xmin committed */ #define HEAP_XMIN_INVALID 0x0200 /* t_xmin invalid/aborted */ #define HEAP_XMAX_COMMITTED 0x0400 /* t_xmax committed */ #define HEAP_XMAX_INVALID 0x0800 /* t_xmax invalid/aborted */
При чтении или записи кортежа PostgreSQL по возможности в t_informask кортежа устанавливает Hint Bits. Например, предположим, что PostgreSQL проверяет статус t_xmin кортежа и получает статус COMMITTED. В этом случае PostgreSQL устанавливает HEAP_XMIN_COMMITTED в t_infomask кортежа. Если Hint Bits уже установлены, то TransactionIdDidCommit и TransactionIdDidAbort больше не нужны. Таким образом, PostgreSQL может беспрепятственно проверить состояние t_xmin и t_xmax каждого кортежа.
Фантомное чтение при уровне изоляции REPEATABLE READ
Уровень изоляции REPEATABLE READ не допускает не только неповторяющегося, но и фантомного чтения (хотя и не обеспечивает полную изоляцию).
Предположим, что две транзакции, Tx_A и Tx_B, выполняются параллельно. Их уровни изоляции - READ COMMITTED и REPEATABLE READ, а их txids - 100 и 101 соответственно.
Tx_B выполняет команду SELECT; однако кортеж, вставленный Tx_A, является невидимым в соответствии с правилом 5. Таким образом, фантомного чтения не происходит.
- Rule5(new tuple): Status(t_xmin:100) = COMMITTED ∧∧ Snapshot(t_xmin:100) = active ⇒⇒ Invisible
testdb=# -- Tx_A: txid 100
testdb=# START TRANSACTION
testdb-# ISOLATION LEVEL READ COMMITTED;
START TRANSACTION
testdb=# SELECT txid_current();
txid_current
--------------
100
(1 row)
testdb=# INSERT INTO tbl(id, data)
VALUES (1,'phantom');
INSERT 1
testdb=# COMMIT;
COMMIT
testdb=# -- Tx_B: txid 101
testdb=# START TRANSACTION
testdb-# ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION
testdb=# SELECT txid_current();
txid_current
--------------
101
(1 row)
testdb=# SELECT * FROM tbl WHERE id=1; id | data ----+------ (0 rows)




