Уровень 3.0
Практика. Транзакции
Подготовительный этап
-
Откройте два терминала. Чтобы их можно было различать, измените приглашение командной строки. В первом терминале задайте переменную
PS1так, чтобы она заменяла\h(имя хоста) наTERM1:[student@ServerName ~]$ PS1=$(echo $PS1 | sed 's/\\h/TERM1/')И выполните то же самое для пользователя postgres:
[student@TERM1 ~]$ sudo su - postgres[postgres@ServerName ~]$ PS1=$(echo $PS1 | sed 's/\\h/TERM1/')Во втором терминале выполните то же самое, но с
TERM2:[student@ServerName ~]$ PS1=$(echo $PS1 | sed 's/\\h/TERM2/')[student@TERM2 ~]$ sudo su - postgres[postgres@ServerName ~]$ PS1=$(echo $PS1 | sed 's/\\h/TERM2/') -
В третьей вкладке или терминале запустите оболочку от имени
postgresи зайдите в сеанс psql:[student@ServerName ~]$ sudo su - postgres[postgres@ServerName ~]$ psqlpsql (15.5)Type "help" for help.postgres=# -
Сбросьте все настройки в
postgresql.auto.conf, выполнив командуALTER SYSTEM RESET ALL:postgres=# ALTER SYSTEM RESET ALL;ALTER SYSTEM -
В первом сеансе
studentперезапустите сервер PostgreSQL:[student@TERM1 ~]$ sudo systemctl restart postgresql -
Перезапустите сессию сессию
postgresв клиенте psql в третьем сеансе:postgres=# \cYou are now connected to database "postgres" as user "postgres".postgres=# -
Удалите все БД, кроме
template1,template0иpostgres:postgres=# \lList of databasesName | Owner | Encoding | Collate | Ctype | ICU Locale | Locale Provider | Access privileges-----------+----------+----------+-------------+-------------+------------+-----------------+-----------------------postgres | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | | libc |student | student | UTF8 | en_US.UTF-8 | en_US.UTF-8 | | libc |template0 | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | | libc | =c/postgres +| | | | | | | postgres=CTc/postgrestemplate1 | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | | libc | =c/postgres +| | | | | | | postgres=CTc/postgres(4 rows)postgres=# DROP DATABASE student;DROP DATABASEЭти базы данных были удалены в качестве примера. В вашей системе набор БД может быть другой, получите его в сеансе
postgresв psql метакомандой\l. -
В СУБД должны быть зарегистрированы только две роли:
postgresиstudent. Все остальные роли должны быть удалены:postgres=# \duList of rolesRole name | Attributes | Member of-----------+------------------------------------------------------------+-----------dbuser1 | | {}postgres | Superuser, Create role, Create DB, Replication, Bypass RLS | {}student | | {}postgres=# DROP ROLE dbuser1;DROP ROLEМетакоманда
\duвыводит в psql список зарегистрированных в СУБД ролей. -
Создайте БД
student, владельцем которой будет рольstudent:postgres=# CREATE DATABASE student OWNER student;CREATE DATABASEpostgres=# \l studentList of databasesName | Owner | Encoding | Collate | Ctype | ICU Locale | Locale Provider | Access | privileges---------+---------+----------+------------+------------+------------+-----------------+------------student | student | UTF8 | en_US.UTF8 | en_US.UTF8 | | libc |(1 row)
Основной этап
-
В первом терминале зайдите в сеанс ролью
student, подключившись к одноименной БД:[postgres@TERM1 ~]$ psql student studentpsql (15.5)Type "help" for help.student@student=> SELECT user;user---------student(1 row)student@student=> SELECT current_catalog;current_catalog-----------------student(1 row)Функция SQL
userвозвращает имя роли в сеансе, функцияcurrent_catalog— имя БД, к которой выполнено подключение. -
В первом терминале создайте таблицу
stabс текстовым столбцом:student@student=> CREATE TABLE stab(msg text);CREATE TABLEstudent@student=> \dList of relationsSchema | Name | Type | Owner--------+------+-------+---------public | stab | table | student(1 row)student@student=> \d stabTable "public.stab"Column | Type | Collation | Nullable | Default--------+------+-----------+----------+---------msg | text | | |Метакоманда
\dбез аргументов выдает список таблиц (и других отношений). Если метакоманде\dзадать имя существующей таблицы, она напечатает структуру этой таблицы. -
Начните транзакцию в первом терминале:
student@student=> BEGIN;BEGINОбратите внимание на изменение приглашения командной строки psql. Там появился символ звездочки (
*), который и говорит о том, что в сеансе запущена транзакция. -
Получите номер текущей транзакции в первом терминале:
student@student=*> SELECT pg_current_xact_id();pg_current_xact_id--------------------885(1 row)Функция
pg_current_xact_id()возвращает номер XID транзакции, если он был назначен. Если он не был назначен, назначает. -
Вставьте в таблицу произвольную строку:
student@student=*> INSERT INTO stab VALUES ('Вставка');INSERT 0 1student@student=*> SELECT * FROM stab;msg---------Вставка(1 row)Изменения данных, производимые командами DML, в транзакции видны сразу.
-
Проверьте, видны ли изменения в таблице
stabвне транзакции, обратившись к таблице с запросом на чтение во втором сеансе:[postgres@TERM2 ~]$ psql student studentpsql (15.5)Type "help" for help.student@student=> SELECT * FROM stab;msg-----(0 rows)Обратите внимание, активная пишущая транзакция не блокирует операции чтения, но система MVCC предоставляет командам и транзакциям разные представления данных — снимки. Во втором сеансе снимок данных таблицы
stabне содержит строки, так как транзакция в первом сеансе еще не зафиксирована. -
Зафиксируйте транзакцию в первом сеансе:
student@student=> COMMIT;COMMITПроверьте содержимое таблицы в обоих сеансах.
В первом:
student@student=> SELECT * FROM stab;msg---------Вставка(1 row)Во втором:
student@student=> SELECT * FROM stab;msg---------Вставка(1 row)После фиксации транзакции внесенные ею изменения в данных видны всем командам и транзакциям.
Откат при ошибке в командах транзакции
-
Начните транзакцию в первом сеансе, но сделайте в какой-либо команде транзакции ошибку:
student@student=> BEGIN;BEGINstudent@student=*> амбигоус_цомманд;ERROR: syntax error at or near "амбигоус_цомманд"LINE 1: амбигоус_цомманд;^student@student=!> SELECT pg_current_xact_id();ERROR: current transaction is aborted, commands ignored until end of transaction blockПопробуйте зафиксировать транзакцию:
student@student=!> COMMIT;ROLLBACKВ режиме по умолчанию, если в какой-либо команде транзакции произошла ошибка, транзакция отменяется и при попытке ее фиксации происходит откат.
-
Получите текущее значение встроенной переменной psql
ON_ERROR_ROLLBACK:student@student=> \echo :ON_ERROR_ROLLBACKoffУстановите значение этой переменной в
on:student@student=> \set ON_ERROR_ROLLBACK onstudent@student=> \echo :ON_ERROR_ROLLBACKon -
Снова начните транзакцию и допустите ошибку. Проверьте отличия:
student@student=> BEGIN;BEGINstudent@student=*> амбигоус_цомманд;ERROR: syntax error at or near "амбигоус_цомманд" LINE 1: амбигоус_цомманд;^Теперь, несмотря на ошибку, транзакция не перешла в состояние ошибки (приглашение осталось
=*>).student@student=*> SELECT pg_current_xact_id();pg_current_xact_id--------------------886(1 row)student@student=*> COMMIT;COMMITПри
ON_ERROR_ROLLBACK = onперед каждой командой транзакции неявно устанавливает точку сохраненияSAVEPOINT, и при возникновении ошибки происходит не откат всей транзакции, а откат к предыдущей успешной команде транзакции.Но у этого подхода есть существенный минус – значительно возрастает нагрузка на сервер из-за обилия точек отката. Поэтому такой настройкой не следует злоупотреблять.
-
Сбросьте
ON_ERROR_ROLLBACK:student@student=> \unset ON_ERROR_ROLLBACKstudent@student=> \echo :ON_ERROR_ROLLBACKoff
Версии строк
Посмотрим на механизм версионирования.
-
Начните транзакцию в первом терминале и получите ее номер:
student@student=> BEGIN;BEGINstudent@student=*> SELECT pg_current_xact_id();pg_current_xact_id--------------------887(1 row) -
Выполните запрос, выводящий поля
xminиxmaxиз заголовка строки:student@student=*> SELECT xmin, xmax, * FROM stab;xmin | xmax | msg------+------+---------885 | 0 | Вставка(1 row)Поле
xmin— номер транзакции, создавшей эту версию строки. Строка в примере была создана транзакцией885. Полеxmax— номер транзакции, удалившей версию строки. Пока эту версию строки никакая транзакция не удаляла. -
Во втором терминале также запустите транзакцию и выполните такой же запрос:
student@student=> BEGIN;BEGINstudent@student=*> SELECT xmin, xmax, * FROM stab;xmin | xmax | msg------+------+---------885 | 0 | Вставка(1 row)Пока результат ничем не отличается от транзакции в первом сеансе.
-
В первом сеансе измените строку командой
UPDATE:student@student=*> UPDATE stab SET msg = 'Обновление';UPDATE 1student@student=*> SELECT xmin, xmax, * FROM stab;xmin | xmax | msg------+------+------------887 | 0 | Обновление(1 row)Теперь
xmin = 887– номер текущей транзакции, создавшей новую версию строки. Старая версия стала невидимой внутри этой транзакции. -
В снимке данных второй транзакции до сих пор должно быть видно старую версию строки, ведь фиксации первой транзакции не было:
student@student=*> SELECT xmin, xmax, * FROM stab;xmin | xmax | msg------+------+---------885 | 887 | Вставка(1 row)Что произошло? Во втором сеансе видно старую версию строки, созданную транзакцией
885, которая была ранее зафиксирована. Полеxmaxтеперь равно887– номеру транзакции, которая удалила эту версию (черезUPDATE). Новая версия пока не видна, так как транзакция887не зафиксирована.Команда
UPDATEэквивалентна действиюDELETE, удаляющей версию строки (помечаяxmaxномером транзакции), плюсINSERT, добавляющей новую версию строки (помечаяxminномером этой же транзакции). -
В первом терминале зафиксируйте транзакцию:
student@student=*> COMMIT;COMMITВо втором терминале в активной транзакции проверьте изменения:
student@student=*> SELECT xmin, xmax, * FROM stab;xmin | xmax | msg------+------+------------887 | 0 | Обновление(1 row)Во втором сеансе транзакция работает на уровне изоляции
Read Committed. На этом уровне изоляции снимки данных строятся для каждой команды транзакции отдельно. После фиксации транзакции в первом сеансе новый снимок во втором сеансе включил зафиксированные изменения. -
Откатите транзакцию во втором терминале:
student@student=*> ROLLBACK;ROLLBACKstudent@student=> SELECT xmin, xmax, * FROM stab;xmin | xmax | msg------+------+------------887 | 0 | Обновление(1 row)
Уровни изоляции
-
Проверьте, какой уровень изоляции установлен по умолчанию:
student@student=> \dconfig def*iso*List of configuration parametersParameter | Value-------------------------------+----------------default_transaction_isolation | read committed(1 row) -
В первом сеансе запустите транзакцию на уровне изоляции
Repeatable Read:student@student=> BEGIN ISOLATION LEVEL REPEATABLE READ;BEGINstudent@student=*> SELECT * FROM stab;msg------------Обновление(1 row)Снимок данных строится при вызове первой команды в транзакции. В данном случае
SELECT. Так как уровень изоляции установленRepeatable Read, снимок, построенный для первой команды транзакции, будет оставаться неизменным до ее завершения. -
Во втором терминале выполните команду
INSERTи прочитайте таблицу:student@student=> INSERT INTO stab VALUES ('Еще вставка');INSERT 0 1student@student=> SELECT * FROM stab;msg-------------ОбновлениеЕще вставка(2 rows)В psql по умолчанию работает режим автофиксации:
student@student=> \echo :AUTOCOMMITonПоэтому выполнение команд DML, изменяющих данные, автоматически фиксируется после каждой выполненной команды.
-
Вернитесь в первый сеанс и повторите запрос:
student@student=*> SELECT * FROM stab;msg------------Обновление(1 row)Изменения, внесенные
INSERT, зафиксированы, но их не видно в снимке данных транзакции в первом сеансе, так как снимок был получен до выполненияINSERT. -
Зафиксируйте транзакцию:
student@student=*> COMMIT;COMMITstudent@student=> SELECT * FROM stab;msg-------------ОбновлениеЕще вставка(2 rows)Снимок, сделанный во время работы транзакции, больше не существует, поэтому данные стали видны.
Блокировки
-
В первом сеансе запустите транзакцию и измените
UPDATEстроку «Обновление», переведя ее в верхний регистр:student@student=> BEGIN;BEGINstudent@student=*> UPDATE stab SET msg = upper(msg) WHERE msg ~ '^Об';UPDATE 1student@student=*> SELECT * FROM stab;msg-------------Еще вставкаОБНОВЛЕНИЕ(2 rows) -
Во втором сеансе проверьте, блокируется ли чтение таблицы:
student@student=> SELECT * FROM stab;msg-------------ОбновлениеЕще вставка(2 rows)Чтение не блокируется изменением строк.
-
Во втором сеансе выполните очистку командой
VACUUM:student@student=> VACUUM VERBOSE stab;INFO: vacuuming "student.public.stab"INFO: finished vacuuming "student.public.stab": index scans: 0pages: 0 removed, 1 remain, 1 scanned (100.00% of total)tuples: 1 removed, 2 remain, 0 are dead but not yet removable, oldest xmin: 800removable cutoff: 800, which was 1 XIDs old when operation endednew relfrozenxid: 798, which is 3 XIDs ahead of previous valuefrozen: 0 pages from table (0.00% of total) had 0 tuples frozenindex scan not needed: 0 pages from table (0.00% of total) had 0 dead item identifiers removedavg read rate: 36.722 MB/s, avg write rate: 36.722 MB/sbuffer usage: 17 hits, 4 misses, 4 dirtiedWAL usage: 5 records, 4 full page images, 26909 bytessystem usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 sINFO: vacuuming "student.pg_toast.pg_toast_16391"INFO: finished vacuuming "student.pg_toast.pg_toast_16391": index scans: 0pages: 0 removed, 0 remain, 0 scanned (100.00% of total)tuples: 0 removed, 0 remain, 0 are dead but not yet removable, oldest xmin: 800removable cutoff: 800, which was 1 XIDs old when operation endednew relfrozenxid: 800, which is 5 XIDs ahead of previous valuefrozen: 0 pages from table (100.00% of total) had 0 tuples frozenindex scan not needed: 0 pages from table (100.00% of total) had 0 dead item identifiers removedavg read rate: 35.034 MB/s, avg write rate: 70.067 MB/sbuffer usage: 24 hits, 1 misses, 2 dirtiedWAL usage: 3 records, 2 full page images, 14340 bytessystem usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 sVACUUMКоманда очистки не мешает параллельному выполнению запросов.
-
Во втором терминале выполните команду
UPDATEстроки «Еще вставка»:student@student=> UPDATE stab SET msg = upper(msg) WHERE msg ~ '^Ещ';UPDATE 1student@student=> SELECT * FROM stab;msg-------------ОбновлениеЕЩЕ ВСТАВКА(2 rows)Так как транзакция в первом сеансе изменила строку «Обновление» и она еще не зафиксирована, то эта строка заблокирована. Но все остальные строки — нет.
-
Попробуйте во втором сеансе обновить строку, которая в данный момент заблокирована транзакцией в первом сеансе:
student@student=> UPDATE stab SET msg = upper(msg) WHERE msg ~ '^Об';Команда «зависла» и не выполняется. На самом деле она ожидает освобождения блокировки, удерживаемой транзакцией в первом сеансе.
-
Зафиксируйте транзакцию в первом сеансе. При этом во втором сеансе команда
UPDATEвыполнит свою работу.В первом терминале:
student@student=*> COMMIT;COMMITВо втором терминале:
UPDATE 0
Очистка
-
В первом терминале запустите транзакцию, а в ней выполните команду
SELECT:student@student=> BEGIN;BEGINstudent@student=*> SELECT * FROM stab;msg-------------ОБНОВЛЕНИЕЕЩЕ ВСТАВКА(2 rows) -
Во втором терминале выполните команду
VACUUM FULLдля таблицы:student@student=> VACUUM FULL stab;Команда не завершается.
VACUUM FULLв отличие от обычногоVACUUMтребует эксклюзивной блокировки таблицы, так как она перестраивает таблицу заново. Запрос чтения в первом сеансе удерживает блокировку, которая препятствует выполнениюVACUUM FULL. -
Завершите транзакцию и проверьте, отработает ли
VACUUM FULL.В первом терминале:
student@student=*> END;COMMITВо втором терминале:
VACUUMПосле освобождения блокировки, наложенной командой
SELECT, команда очисткиVACUUM FULLотработала и вернула командную строку.
На этом лабораторную работу можно считать завершенной.
Самопроверка
Вопрос 1
Открыто два сеанса psql. Первый сеанс обслуживается процессом с pid=13646, второй — процессом с pid=13917.
У каждого из сеансов в начале приглашения указывается pid обслуживающего процесса.
В указанных сеансах последовательно выполняются следующие команды:
13646 test_db=# BEGIN;
BEGIN
13646 test_db=*# CREATE TABLE test_table AS SELECT 1 id;
SELECT 1
13646 test_db=*# UPDATE test_table SET id=2;
UPDATE 1
13917 test_db=# SELECT id FROM test_table;
Каким будет результат выполнения последнего запроса?
Вопрос 2
Имеется таблица с данными test_table:
test_db=# \d test_table
Таблица "public.test_table"
Столбец | Тип | Правило сортировки | Допустимость NULL | По умолчанию
---------+---------+--------------------+-------------------+--------------
id | integer | | |
test_db=# SELECT * FROM test_table;
id
----
1
(1 строка)
Открыто два сеанса psql. Первый сеанс обслуживается процессом с pid=13646, второй — процессом с pid=13917.
У каждого из сеансов в начале приглашения указывается pid обслуживающего процесса.
В указанных сеансах последовательно выполняются следующие команды:
13646 test_db=# BEGIN ISOLATION LEVEL REPEATABLE READ;
BEGIN
13646 test_db=*# SELECT id FROM test_table;
id
----
1
(1 строка)
13917 test_db=# BEGIN;
BEGIN
13917 test_db=*# UPDATE test_table SET id=2;
UPDATE 1
13917 test_db=*# COMMIT;
COMMIT
13646 test_db=*# SELECT id FROM test_table;
Какое значение id будет получено в результате выполнения последнего запроса?
Вопрос 3
Открыто два сеанса psql. Первый сеанс обслуживается процессом с pid=13646, второй — процессом с pid=13917.
У каждого из сеансов в начале приглашения указывается pid обслуживающего процесса.
В указанных сеансах последовательно выполняются следующие команды:
13646 test_db=# BEGIN;
BEGIN
13646 test_db=*#update test_table set id=3;
UPDATE 1
13646 test_db=*# SELECT pg_current_xact_id();
pg_current_xact_id
--------------------
795
(1 строка)
13917 test_db=# BEGIN;
BEGIN
13917 test_db=# SELECT pg_current_xact_id();
pg_current_xact_id
--------------------
796
(1 строка)
13917 test_db=# SELECT id, xmin, xmax FROM test_table;
id | xmin | xmax
----+------+------
2 | 794 | 795
(1 строка)
Транзакция с каким номером заблокировала единственную строку в таблице test_table?
Вопрос 4
Имеется таблица с данными test_table:
test_db=# \d test_table
Таблица "public.test_table"
Столбец | Тип | Правило сортировки | Допустимость NULL | По умолчанию
---------+---------+--------------------+-------------------+--------------
id | integer | | |
test_db=# SELECT * FROM test_table;
id
----
1
(1 строка)
Открыто два сеанса psql. Первый сеанс обслуживается процессом с pid=13646, второй — процессом с pid=13917.
У каждого из сеансов в начале приглашения указывается pid обслуживающего процесса.
В указанных сеансах последовательно выполняются следующие команды:
13646 test_db=# BEGIN;
BEGIN
13917 test_db=# BEGIN;
BEGIN
13917 test_db=*# DROP TABLE test_table;
DROP TABLE
13646 test_db=*# SELECT id FROM test_table;
Что произойдет после вызова последней команды?