Углубленный анализ управления параллелизмом и механизма транзакций в серии PostgreSQL.

база данных PostgreSQL
Углубленный анализ управления параллелизмом и механизма транзакций в серии PostgreSQL.

Углубленный анализ семейства PostgreSQLОрганизовано изThe Internals of PostgreSQLПодождите, пока серия статей, от фрагментации систематизированного обучения, более глубокого проникновения в базу данных, касаясь обхода, взаимного подтверждения, а также облегчения других реляционных баз данных, таких как MySQL или база данных NOSQL.

Углубленный анализ управления параллелизмом и механизма транзакций в серии PostgreSQL.

Контроль параллелизма направлен на обеспечение согласованности и изоляции в ACID для сценариев, в которых транзакции распараллелены в базе данных. Тремя основными технологиями управления параллелизмом в технологии баз данных являются: многоверсионный контроль параллелизма (MVCC), строгая двухфазная блокировка (S2PL) и оптимистичный контроль параллелизма (OCC), каждая из которых имеет множество вариантов. В MVCC каждая операция записи создает новую версию поверх старой версии и сохраняет старую версию. Когда транзакции необходимо прочитать данные, система базы данных выберет из всех версий ту версию, которая соответствует требованиям уровня изоляции транзакций. Самым большим преимуществом MVCC является то, что чтение не блокирует запись, а запись не блокирует чтение; в то время как в системе, подобной S2PL, транзакции записи заранее приобретут эксклюзивные блокировки, которые заблокируют транзакции чтения.

РСУБД, такие как PostgreSQL и Oracle, на самом деле используют вариант метода MVCC, называемый Snapshot Isolation (SI). Oracle представила дополнительные сегменты отката: при записи новых данных старая версия данных будет записана в сегмент отката, а затем перезаписана в фактический блок данных. PostgreSQL использует относительно простую реализацию, и новые объекты данных будут вставлены непосредственно в связанную страницу таблицы, а при чтении данных таблицы PostgreSQL выберет соответствующую версию.

SI может избежать трех аномалий, определенных в стандарте ANSI SQL-92: грязное чтение, неповторяемое чтение и фантомное чтение; изоляция сериализуемых моментальных снимков (SSI) может обеспечить настоящие возможности последовательного чтения и записи.

Isolation Level Dirty Reads Non-repeatable Read Phantom Read Serialization Anomaly
READ COMMITTED Not possible Possible Possible Possible
REPEATABLE READ Not possible Not possible Not possible in PG; See Section 5.7.2. (Possible in ANSI SQL) Possible
SERIALIZABLE Not possible Not possible Not possible Not possible

Структура кортежа

Transaction ID

Когда транзакция включена, встроенный диспетчер транзакций POSTGRESQL присвоит ей уникальный идентификатор транзакции (TXID); TXID — это 32-битное целочисленное значение, не имеющее типа, которое можетtxid_current()Функция для получения текущего txid:

testdb=# BEGIN;
BEGIN
testdb=# SELECT txid_current();
 txid_current
--------------
          100
(1 row)

PostgreSQL также резервирует три ключевых значения txid для специальной маркировки: 0 представляет собой недопустимый txid, 1 представляет собой txid при запуске, который используется только при запуске кластера баз данных; 2 представляет собой замороженный (Frozen) txid, используемый в используемой сериализации. во время бизнеса. PostgreSQL выбирает числовой тип txid для удобства сравнения; для транзакций со значением txid 100 все транзакции меньше 100 произошли в прошлом и являются видимыми, а все транзакции больше 100 произошли в будущем, то есть невидимы.

image

Поскольку количество txid, требуемых в реальной системе, может превышать максимально допустимое, PostgreSQL фактически рассматривает эти txid как кольца.

HeapTupleHeaderData

Кортежи кучи на страницах таблиц часто состоят из трех частей: структуры HeapTupleHeaderData, растрового изображения NULL и пользовательских данных.

image

Свойства HeapTupleHeaderData, тесно связанные с обработкой транзакций:

  • (TransactionId)t_xmin: сохранить txid при вставке кортежа
  • (TransactionId)t_xmax: сохранить txid при удалении или обновлении кортежа. Если он не был обновлен или удален, установите для него значение 0, что означает, что он недействителен.
  • (CommandId)t_cid: хранит идентификатор команды, то есть количество всех команд SQL, выполненных в рамках транзакции командой, создавшей кортеж; напримерBEGIN; INSERT; INSERT; INSERT; COMMIT;Эта транзакция, если это Tuple, созданный первой командой INSERT, то ее значение t_cid равно 0, а второе — 1
  • (ItemPointerData)t_ctid: когда кортеж обновляется, значение указывает на вновь созданный кортеж, в противном случае оно указывает на себя

Вставка, удаление и обновление кортежа

Как упоминалось выше, кортежи на страницах таблиц располагаются следующим образом:

image

вставлять

При операции вставки PostgreSQL направит новый кортеж страницы, вставленный в целевую таблицу:

image

Если транзакция с txid 99 вставляет новый кортеж, в поле заголовка кортежа будет установлено следующее значение:

  • t_xmin согласуется с txid транзакции, создавшей кортеж, который равен 99.
  • t_xmax установлен на 0, потому что он не был удален или обновлен
  • t_cid имеет значение 0, поскольку Tuple был создан первой командой Insert в транзакции.
  • t_ctid установлен на(0, 1), указывая на себя
testdb=# CREATE EXTENSION pageinspect;
CREATE EXTENSION
testdb=# CREATE TABLE tbl (data text);
CREATE TABLE
testdb=# INSERT INTO tbl VALUES('A');
INSERT 0 1
testdb=# SELECT lp as tuple, t_xmin, t_xmax, t_field3 as t_cid, t_ctid
                FROM heap_page_items(get_raw_page('tbl', 0));
 tuple | t_xmin | t_xmax | t_cid | t_ctid
-------+--------+--------+-------+--------
     1 |     99 |      0 |     0 | (0,1)

Удалить

В операции удаления целевой кортеж будет логически удален первым, то есть значение t_xmax устанавливается равным значению txid транзакции, которая в данный момент удаляет кортеж.

image

После того, как транзакция будет зафиксирована, PostgreSQL пометит кортеж как Dead Tuple, а затем будет полностью очищен во время обработки VACUUM.

возобновить

Во время операции обновления PostgreSQL сначала логически удалит последний кортеж, а затем вставит новый:

image

Строка, показанная на рисунке выше, была вставлена ​​транзакцией с txid 99 и дважды подряд обновлена ​​транзакцией с txid 100; после фиксации транзакции Tuple_2 и Tuple_3 будут помечены как Dead Tuples.

Free Space Map

При вставке кортежа кучи или кортежа индекса PostgreSQL использует FSM связанной таблицы, чтобы решить, какую страницу следует выбрать для конкретной операции вставки. Каждый FSM хранит информацию об оставшейся емкости пространства, относящейся к таблице или индексному файлу, которую можно просмотреть следующими способами:

testdb=# CREATE EXTENSION pg_freespacemap;
CREATE EXTENSION

testdb=# SELECT *, round(100 * avail/8192 ,2) as "freespace ratio"
                FROM pg_freespace('accounts');
 blkno | avail | freespace ratio
-------+-------+-----------------
     0 |  7904 |           96.00
     1 |  7520 |           91.00
     2 |  7136 |           87.00
     3 |  7136 |           87.00
     4 |  7136 |           87.00
     5 |  7136 |           87.00
....

Commit Log

PostgreSQL использует журнал фиксации, также известный как засорение, для хранения статуса транзакций; засоры хранятся в общей памяти и играют важную роль во всем жизненном цикле обработки транзакций. PostgreSQL определяет четыре разных состояния транзакции: IN_PROGRESS, COMMITTED, ABORTED и SUB_COMMITTED.

Clog состоит из нескольких страниц по 8 КБ в общей памяти, которая логически представляет собой структуру, подобную массиву.Нижний индекс массива — это txid связанной транзакции, а значение — статус текущей транзакции:

image

Если текущий txid превышает максимальный диапазон, который может содержать текущая страница засорения, PostgreSQL автоматически создаст новую страницу. Когда PostgreSQL остановлен или запущен процесс Checkpoint, данные засорения будут постоянно храниться в подкаталоге pg_xact с именами в порядке 0000, 0001, а максимальный размер одного файла — 256 КБ. При перезапуске PostgreSQL файлы, хранящиеся в каталоге pg_xact, будут перезагружены в память. При непрерывной работе PostgreSQL в засоре неизбежно будет скапливаться много устаревших или бесполезных данных, и эти бесполезные данные также будут очищаться при обработке Vacuum.

Снимок транзакции | Снимок транзакции

Снимок транзакций — это структура данных, в которой хранится информация о том, активны ли все текущие транзакции.Внутренне PostgreSQL представляет снимок в виде простой текстовой структуры.xmin:xmax:xip_list’, например "100:100:", что означает, что все транзакции с txid меньше или равным 99 неактивны, а транзакции больше или равные 100 активны.

testdb=# SELECT txid_current_snapshot();
 txid_current_snapshot
-----------------------
 100:104:100,102
(1 row)
  • xmin: самый ранний txid, который все еще находится в активном состоянии.Все более ранние транзакции находятся либо в видимом состоянии после фиксации, либо в приостановленном состоянии после отката.
  • xmax: номер первой еще не выделенной транзакции Все транзакции с txid больше или равным этому значению еще не произошли относительно транзакции, которой принадлежит снимок, поэтому они невидимы.
  • xip_list: txids, которые активны во время моментального снимка, будут включены только txids между xmin и xmax.

от100:104:100,102Например, схематическая диаграмма выглядит следующим образом:

image

Моментальные снимки транзакций в основном предоставляются диспетчером транзакций (Диспетчер транзакций).На уровне изоляции READ COMMITTED транзакция будет назначена снимку независимо от того, выполняется ли команда SQL.Для транзакций на уровне изоляции REPEATABLE READ или SERIALIZABLE Только при выполнении первого оператора SQL он назначается моментальному снимку транзакции для обнаружения видимости. Значение моментальных снимков транзакций заключается в том, что когда моментальный снимок выполняет оценку видимости, независимо от того, была ли целевая транзакция зафиксирована или отменена, пока она помечена как активная в моментальном снимке, она будет рассматриваться как транзакция в состоянии IN_PROGRESS.

image

Менеджер транзакций всегда сохраняет информацию о текущих операциях. Предполагая, что три сделки начинают одно место, а уровень изоляции транзакций_а и транзакции_b прочитан преданный, уровень изоляции транзакции_c является повторяемым чтением.

  • Т1:

    • Transaction_A запускает и выполняет первую команду SELECT. При выполнении первой команды Transaction_A запрашивает txid и моментальный снимок на данный момент. В этом случае диспетчер транзакций назначает txid 200 и возвращает моментальный снимок транзакции «200:200:».
  • Т2:

    • Transaction_B запускает и выполняет первую команду SELECT. Диспетчер транзакций присваивает txid 201 и возвращает моментальный снимок транзакции «200:200:», поскольку выполняется Transaction_A (txid 200). Следовательно, Transaction_A нельзя увидеть из Transaction_B.
  • Т3:

    • Transaction_C запускает и выполняет первую команду SELECT. Диспетчер транзакций присваивает txid 202 и возвращает моментальный снимок транзакции «200:200:», поэтому Transaction_A и Transaction_B не видны из Transaction_C.
  • Т4:

    • Транзакция_A отправлена. Менеджер транзакций удаляет информацию об этой транзакции.
  • Т5:

    • Transaction_B и Transaction_C выполняют соответствующие команды SELECT.
    • Transaction_B требует моментального снимка транзакции, поскольку он находится на уровне READ COMMITTED. В этом случае транзакция_B получает новый снимок «201:201:», поскольку транзакция_A (txid 200) зафиксирована. Поэтому Transaction_B больше не является невидимой в Transaction_B.
    • Transaction_C не нуждается в моментальном снимке транзакции, поскольку он находится на уровне REPEATABLE READ и использует полученный моментальный снимок, т. е. «200:200:». Поэтому Transaction_A по-прежнему невидима для Transaction_C.

Проверка видимости | Проверка видимости

Правила |Правила определения видимости

Правила обнаружения видимости используются для определения того, является ли кортеж видимым для транзакции, на основе t_xmin и t_xmax кортежа, засорения и моментального снимка транзакции, для которого он выделен.

Статус транзакции, соответствующей t_xmin, ABORTED

Когда состояние транзакции, соответствующее значению t_xmin кортежа, равно ABORTED, кортеж никогда не отображается:

/* t_xmin status = ABORTED */
// Rule 1: If Status(t_xmin) = ABORTED ⇒ Invisible
Rule 1: IF t_xmin status is 'ABORTED' THEN
                  RETURN 'Invisible'
            END IF

Статус транзакции, соответствующей t_xmin, — IN_PROGRESS.

Для кортежей, которые не связаны с транзакциями, отличными от транзакции, вставившей кортеж, кортеж никогда не виден; он виден только для кортежей, принадлежащих к той же транзакции, что и кортеж (в настоящее время кортеж не удаляется и не обновляется). .

 /* t_xmin status = IN_PROGRESS */
              IF t_xmin status is 'IN_PROGRESS' THEN
            	   IF t_xmin = current_txid THEN
// Rule 2: If Status(t_xmin) = IN_PROGRESS ∧ t_xmin = current_txid ∧ t_xmax = INVAILD ⇒ Visible
Rule 2:              IF t_xmax = INVALID THEN
			      RETURN 'Visible'
// Rule 3: If Status(t_xmin) = IN_PROGRESS ∧ t_xmin = current_txid ∧ t_xmax ≠ INVAILD ⇒ Invisible

Rule 3:              ELSE  /* this tuple has been deleted or updated by the current transaction itself. */
			      RETURN 'Invisible'
                         END IF
// Rule 4: If Status(t_xmin) = IN_PROGRESS ∧ t_xmin ≠ current_txid ⇒ Invisible
Rule 4:        ELSE   /* t_xmin ≠ current_txid */
		          RETURN 'Invisible'
                   END IF
             END IF

Статус транзакции, соответствующей t_xmin, — COMMITTED.

В этот момент в кортеже они видны в большинстве случаев, подлежат обновлению или удалению вдобавок к кортежу.

 /* t_xmin status = COMMITTED */
            IF t_xmin status is 'COMMITTED' THEN
//  If Status(t_xmin) = COMMITTED ∧ Snapshot(t_xmin) = active ⇒ Invisible
Rule 5:      IF t_xmin is active in the obtained transaction snapshot THEN
                      RETURN 'Invisible'
// If Status(t_xmin) = COMMITTED ∧ (t_xmax = INVALID ∨ Status(t_xmax) = ABORTED) ⇒ Visible
Rule 6:      ELSE IF t_xmax = INVALID OR status of t_xmax is 'ABORTED' THEN
                      RETURN 'Visible'
            	 ELSE IF t_xmax status is 'IN_PROGRESS' THEN
// If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = IN_PROGRESS ∧ t_xmax = current_txid ⇒ Invisible
Rule 7:           IF t_xmax =  current_txid THEN
                            RETURN 'Invisible'
// If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = IN_PROGRESS ∧ t_xmax ≠ current_txid ⇒ Visible
Rule 8:           ELSE  /* t_xmax ≠ current_txid */
                            RETURN 'Visible'
                      END IF
            	 ELSE IF t_xmax status is 'COMMITTED' THEN
// If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = COMMITTED ∧ Snapshot(t_xmax) = active ⇒ Visible
Rule 9:           IF t_xmax is active in the obtained transaction snapshot THEN
                            RETURN 'Visible'
// If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = COMMITTED ∧ Snapshot(t_xmax) ≠ active ⇒ Invisible
Rule 10:         ELSE
                            RETURN 'Invisible'
                      END IF
            	 END IF
            END IF

Процесс проверки видимости

Возьмем в качестве примера простое обновление и запрос с двумя транзакциями:

image

Уровень изоляции транзакции txid 200 на приведенном выше рисунке — READ COMMITED, а уровень изоляции txid 201 — READ COMMITED или REPEATABLE READ.

  • При выполнении команды SELECT в момент времени T3:

Согласно правилу 6, в настоящее время толькоTuple_1находится в видимом состоянии:

# 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:

Для транзакции txid 200, согласно правилу 7 и правилу 2,Tuple_1видимый иTuple_2Невидимый:

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

А для транзакции txid 201,Tuple_1видно,Tuple_2невидим:

# 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)
  • При выполнении команды SELECT в момент времени T7:

Если транзакция с txid 201 в это время находится на уровне изоляции READ COMMITED, то txid 200 будет рассматриваться как COMMITTED, потому что моментальный снимок транзакции, полученный в это время,201:201:,следовательноTuple_1невидим, иTuple_2видно:

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

Если TXID 201 в это время транзакции является повторяющийся уровень изоляции чтения, на этот раз, чтобы получить снимок или транзакционную200:200:, то транзакция с txid 200 должна рассматриваться как состояние IN_PROGRESS, поэтому в настоящее времяTuple_1видно, иTuple_2невидим:

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

Предотвращение потери обновлений | Предотвращение потери обновлений

Так называемый Lost Update, или ww-конфликт, возникает, когда две транзакции одновременно обновляют одну и ту же строку; в PostgreSQL оба уровня REPEATABLE READ и SERIALIZABLE должны избегать этой аномалии.

(1)  FOR each row that will be updated by this UPDATE command
(2)       WHILE true

               /* The First Block */
(3)            IF the target row is being updated THEN
(4)	              WAIT for the termination of the transaction that updated the target row

(5)	              IF (the status of the terminated transaction is COMMITTED)
   	                   AND (the isolation level of this transaction is REPEATABLE READ or SERIALIZABLE) THEN
(6)	                       ABORT this transaction  /* First-Updater-Win */
	              ELSE
(7)                           GOTO step (2)
	              END IF

               /* The Second Block */
(8)            ELSE IF the target row has been updated by another concurrent transaction THEN
(9)	              IF (the isolation level of this transaction is READ COMMITTED THEN
(10)	                       UPDATE the target row
	              ELSE
(11)	                       ABORT this transaction  /* First-Updater-Win */
	              END IF

               /* The Third Block */
                ELSE  /* The target row is not yet modified or has been updated by a terminated transaction. */
(12)	              UPDATE the target row
                END IF
           END WHILE
      END FOR

В приведенном выше процессе команда UPDATE проходит через каждую строку, которую нужно обновить, и когда обнаруживает, что строка обновляется другими транзакциями, она переходит в состояние ожидания, пока строка не будет разблокирована. Если строка уже была обновлена ​​и уровень изоляции REPEATABLE или SERIALIZABLE, отмените обновление.

image

Обновление означает, что строка была обновлена ​​другой параллельной транзакцией, и ее транзакция еще не завершена. Поскольку SI Postgres Pro использует схему «выиграл первый обновляющий», в этом случае текущая транзакция должна дождаться завершения транзакции, обновившей целевую строку. Предположим, что транзакции Tx_A и Tx_B выполняются одновременно, и Tx_B пытается обновить строку, но Tx_A обновил ее и все еще выполняется, а Tx_B ожидает завершения Tx_A. Продолжает операцию обновления текущей транзакции после обновления транзакции, зафиксированной целевой строкой. Если текущая транзакция находится на уровне READ COMMITTED, целевая строка будет обновлена; в противном случае REPEATABLE READ или SERIALIZABLE текущая транзакция немедленно прерывается, чтобы предотвратить потерю обновлений.

космическая организация

Механизм управления параллелизмом PostgreSQL также зависит от следующих процессов обслуживания:

  • Удалены кортежи и индексные кортежи, помеченные как мертвые
  • Удалить устаревшие части засора
  • Заморозить старые txids
  • Обновите FSM, VM и другую статистику

Давайте сначала обсудим проблему циклической обработки txid, предполагая, что транзакция txid 100 вставляет определенныйTuple_1, значение t_xmin, соответствующее Tuple, равно 100, тогда сервер работает долго,Tuple_1Период не изменен. пока txid не будет2^31 + 101, для транзакции, когда она выполняет команду SELECT, она не может видетьTuple_1, т.к. транзакция с txid 100 происходит в будущем относительно него, созданный ею Tuple естественно невидим.

Чтобы решить эту проблему, PostgreSQL представил так называемый замороженный TXID (замороженный TXID) и настроил процесс замораживания для конкретно справляться с проблемой. Как упомянуто выше, TXID 2 является зарезервированным значением, которое конкретно представляет эти замороженные кортежи, которые всегда неактивны и видны. Процесс замораживания также называется равномерноми процессом вакуума. Он сканирует все таблицы файлов и устанавливает поле T_XMIN кортеж, чья разница от текущего TXID превышает определение Vacuum_Freeze_min_Age на 2. После версии 9.4 бит XMIN_FROZEN в поле T_INFOMASK устанавливается, чтобы указать, что кортеж находится в замороженном состоянии.

дальнейшее чтение