Уровень 3.0
Предусловия:
- Изучена лекция 1 «Свойства транзакций»
Анализ аномалий при различных уровнях изоляции
-
Откройте терминал и подключитесь в psql к базе данных
postgresc рольюpostgres:[student@pkles-gt0040523 ~]$ sudo -iu postgres-bash-4.4$ psqlpsql (15.5)Введите "help", чтобы получить справку. -
Проверьте уровень изоляции по умолчанию:
postgres=# SHOW default_transaction_isolation;default_transaction_isolation-------------------------------read committed(1 строка)Как и ожидалось, уровень изоляции по умолчанию Read Committed.
Если уровень изоляции по умолчанию окажется другим, то сбросьте значение параметра
default_transaction_isolation:postgres=# ALTER SYSTEM RESET default_transaction_isolation;ALTER SYSTEMpostgres=# SELECT pg_reload_conf();pg_reload_conf----------------t(1 строка) -
Создайте пользователя student, базу данных службы доставки и подключитесь к ней:
postgres=# CREATE ROLE student WITH PASSWORD 'student' LOGIN;CREATE ROLEpostgres=# CREATE DATABASE delivery_db OWNER student;CREATE DATABASEpostgres=# \c delivery_db studentВы подключены к базе данных "delivery_db" как пользователь "student". -
Создайте таблицу
hubs, в которой хранится информация о количестве посылок в сортировочных центрах, и наполните ее тестовыми данными:delivery_db=> CREATE TABLE hubs(hub_id integer PRIMARY KEY, city text, packages_amount integer);CREATE TABLEdelivery_db=> INSERT INTO hubs VALUES (1, 'Москва', 300), (2, 'Москва', 200), (3, 'Санкт-Петербург', 400);INSERT 0 3 -
Настройте приглашение в psql с указанием номера обслуживающего процесса:
delivery_db=> \set PROMPT1 '%p%R%x%# '80642=> \set PROMPT2 '%p%R%x%# 'Теперь в приглашении указан
pidобслуживающего процесса. Это необходимо для идентификации разных сеансов psql. -
Откройте второй терминал и подключитесь в psql к базе данных
delivery_dbс ролью student:[student@pkles-gt0040523 ~]$ psql -d delivery_dbpsql (15.5)Введите "help", чтобы получить справку. -
Настройте приглашение во втором сеансе psql с указанием номера обслуживающего процесса:
delivery_db=> \set PROMPT1 '%p%R%x%# '80667=> \set PROMPT2 '%p%R%x%# '
Уровень изоляции Read Committed
Отсутствие грязного чтения
-
В первом сеансе psql начните транзакцию с уровнем изоляции Read Committed и проверьте содержимое таблицы
hubs:80642=> BEGIN;BEGIN80642=*> SELECT * FROM hubs;hub_id | city | packages_amount--------+-----------------+-----------------1 | Москва | 3002 | Москва | 2003 | Санкт-Петербург | 400(3 строки)В данном случае при открытии транзакции явно указывать уровень изоляции не потребовалось, так как уровень Read Committed используется по умолчанию. Обратите внимание, что после открытия транзакции в приглашение psql добавился символ "*".
-
В первом сеансе отправьте 50 посылок из одного сортировочного центра в другой:
80642=*> UPDATE hubs SET packages_amount = packages_amount - 50 WHERE hub_id = 1 RETURNING packages_amount AS msc_amount_1;msc_amount_1--------------250(1 строка)UPDATE 180642=*> UPDATE hubs SET packages_amount = packages_amount + 50 WHERE hub_id = 3 RETURNING packages_amount AS spb_amount_3;spb_amount_3--------------450(1 строка)UPDATE 180642=*> SELECT * FROM hubs;hub_id | city | packages_amount-------+-----------------+-----------------2 | Москва | 2001 | Москва | 2503 | Санкт-Петербург | 450(3 строки)Обратите внимание, что внутри транзакции изменения самой транзакции видны.
-
Во втором сеансе откройте транзакцию с тем же уровнем изоляции Read Committed и проверьте содержимое таблицы
hubs:80667=> BEGIN;BEGIN80667=*> SELECT * FROM hubs;hub_id | city | packages_amount-------+-----------------+-----------------1 | Москва | 3002 | Москва | 2003 | Санкт-Петербург | 400(3 строки)Изменения незафиксированной первой транзакции не видны, значит, аномалия грязного чтения не допускается.
-
В первом сеансе зафиксируйте транзакцию:
80642=*> COMMIT;COMMIT -
Во втором сеансе повторите выполнение запроса:
80667=*> SELECT * FROM hubs;hub_id | city | packages_amount--------+-----------------+-----------------2 | Москва | 2001 | Москва | 2503 | Санкт-Петербург | 450(3 строки)Теперь изменения первой транзакции видны.
-
Во втором сеансе зафиксируйте транзакцию:
80667=*> COMMIT;COMMIT
Неповторяющееся чтение
-
В первом сеансе начните транзакцию с уровнем изоляции Read Committed и отправьте 50 посылок из Санкт-Петербурга в Москву с условием того, что московские сортировочные центры не могут хранить более 300 посылок каждый:
80642=> BEGIN;BEGIN80642=*> DO $$BEGINIF (SELECT packages_amount FROM hubs WHERE hub_id = 1) >= 300-50 THENPERFORM pg_sleep(30);UPDATE hubs SET packages_amount = packages_amount + 50 WHERE hub_id = 1;UPDATE hubs SET packages_amount = packages_amount - 50 WHERE hub_id = 3;END IF;END;$$;Ответ будет выдан по прошествии 30 секунд:
DOДля задания условия предельной емкости сортировочного центра в г. Москва использовался условный оператор в анонимном блоке
DOна языке PL/pgSQL. Для создания искусственной задержки между запросом в условии и командами обновления таблиц использовалась командаPERFORM pg_sleep(30);, выполняющая запрос и отбрасывающая результаты его выполнения. -
Во время «сна» первой транзакции во втором сеансе отправьте 50 посылок из одного сортировочного центра в Москве в другой с фиксацией транзакции:
80667=> BEGIN;BEGIN80667=*> UPDATE hubs SET packages_amount = packages_amount - 50 WHERE hub_id = 2 RETURNING packages_amount AS msc_amount_2;msc_amount_2--------------150(1 строка)UPDATE 180667=*> UPDATE hubs SET packages_amount = packages_amount + 50 WHERE hub_id = 1 RETURNING packages_amount AS msc_amount_1;msc_amount_1--------------300(1 строка)UPDATE 180667=*> COMMIT;COMMIT -
В первом сеансе дождитесь выполнения предыдущего оператора и посмотрите содержимое таблицы
hubs:80642=*> SELECT * FROM hubs;hub_id | city | packages_amount--------+-----------------+-----------------2 | Москва | 1501 | Москва | 3503 | Санкт-Петербург | 400(3 строки)В результате в сортировочном центре с
hub_id=1превышена его емкость (packages_amount=350), то есть нарушено одно из правил согласованности. Это произошло по причине того, что уровень изоляции Read Committed допускает аномалию неповторяющегося чтения. Если бы запрос в условном операторе первой транзакцииSELECT packages_amount FROM hubs WHERE hub_id = 1был бы выполнен повторно после фиксации второй транзакции, он бы выдал другие результаты, и операции обновления в теле условного оператора не были бы выполнены.Такого рода проверки условий с последующим изменением данных с использованием уровня изоляции Read Committed являются распространенным антипаттерном проектирования. Между проверкой условия и командами обновления другие транзакции могут изменить те данные, которые участвовали в проверке, и изменения будут видны командам изменения данных.
Чтобы избежать этого, на уровне изоляции Read Committed рекомендуется использовать атомарные операторы, такие как общие табличные выражения (CTE) или
INSERT ... ON CONFLICT .... В данном случае проблему можно было решить путем использования ограничения целостностиCHECK, проверяющего предельную емкость сортировочных центров в Москве. В отдельных случаях могут использоваться явные блокировки, например с помощью оператораSELECT FOR UPDATE. Однако это сводит на нет преимущества многоверсионности. -
В первом сеансе откатите транзакцию:
80642=*> ROLLBACK;ROLLBACK
Потерянные изменения
-
В первом сеансе начните транзакцию с уровнем Read Committed и получите количество посылок в сортировочном центре в г. Санкт-Петербург с сохранением результата в переменную psql:
80642=> BEGIN;BEGIN80642=*> SELECT packages_amount AS spb_amount FROM hubs WHERE hub_id = 3 \gset80642=*> \echo :spb_amount450 -
То же самое сделайте во втором сеансе:
80667=> BEGIN;BEGIN80667=*> SELECT packages_amount AS spb_amount FROM hubs WHERE hub_id = 3 \gset80667=*> \echo :spb_amount450 -
В первом сеансе добавьте 50 посылок к сортировочному центру в г. Санкт-Петербург и зафиксируйте изменения:
80642=*> UPDATE hubs SET packages_amount = :spb_amount + 50 WHERE hub_id = 3 RETURNING packages_amount AS spb_amount_3;spb_amount_3-----------------500(1 строка)UPDATE 180642=*> COMMIT;COMMIT -
Во втором сеансе сделайте то же самое:
80667=*> UPDATE hubs SET packages_amount = :spb_amount + 50 WHERE hub_id = 3 RETURNING packages_amount AS spb_amount_3;spb_amount_3-----------------500(1 строка)UPDATE 180667=*> COMMIT;COMMITВ результате 50 посылок в сортировочном центре г. Санкт-Петербург утеряно. Это произошло по причине того, что вторая транзакция не учла изменений, выполненных первой транзакцией (хотя они ей были видны), но полагалась на промежуточные результаты, сохраненные в переменной
spb_amount. Это и есть пример аномалии потерянных изменений, которая может возникать таким образом на уровне изоляции Read Committed.
Уровень изоляции Repeatable Read
Отсутствие неповторяющегося чтения
-
В первом сеансе начните транзакцию с уровнем изоляции Read Committed и отправьте из Санкт-Петербурга в Москву 50 посылок:
80642=> BEGIN;BEGIN80642=*> UPDATE hubs SET packages_amount = packages_amount - 50 WHERE hub_id = 3 RETURNING packages_amount AS spb_amount_3;spb_amount_3------------450(1 строка)UPDATE 180642=*> UPDATE hubs SET packages_amount = packages_amount + 50 WHERE hub_id = 2 RETURNING packages_amount AS msc_amount_2;msc_amount_2------------200(1 строка)UPDATE 1 -
Во втором сеансе начните транзакцию с уровнем изоляции Repeatable Read и посмотрите содержимое таблицы
hubs:80667=> BEGIN ISOLATION LEVEL REPEATABLE READ;BEGIN80667=*> SELECT * FROM hubs;hub_id | city | packages_amount--------+-----------------+-----------------1 | Москва | 3002 | Москва | 1503 | Санкт-Петербург | 500(3 строки)Как и ожидалось, незафиксированные изменения первой транзакции не видны.
-
В первом сеансе зафиксируйте транзакцию:
80642=*> COMMIT;COMMIT -
Во втором сеансе повторите выполнение запроса:
80667=*> SELECT * FROM hubs;hub_id | city | packages_amount--------+-----------------+-----------------1 | Москва | 3002 | Москва | 1503 | Санкт-Петербург | 500(3 строки)Несмотря на фиксацию первой транзакции, во второй транзакции изменения по-прежнему не видны. Таким образом, на уровне изоляции Repeatable Read не допускается аномалия неповторяющегося чтения. Согласованность не может быть нарушена в результате фиксации других транзакций в промежутке между выполнением операторов.
Отсутствие фантомного чтения
-
В первом сеансе начните транзакцию с уровнем изоляции Read Committed и добавьте в Санкт-Петербург новый сортировочный центр с фиксацией транзакции:
80642=> BEGIN;BEGIN80642=*> INSERT INTO hubs VALUES(4, 'Санкт-Петербург', 0);INSERT 0 180642=*> COMMIT;COMMIT -
Во втором сеансе транзакции, которая по-прежнему не зафиксирована, посмотрите содержимое таблицы
hubs:80667=*> SELECT * FROM hubs;hub_id | city | packages_amount--------+-----------------+-----------------1 | Москва | 3002 | Москва | 1503 | Санкт-Петербург | 500(3 строки)Добавленная в первой транзакции строка также не видна. Таким образом, на уровне изоляции Repeatable Read также не допускается аномалия фантомного чтения.
-
Во втором сеансе зафиксируйте транзакцию:
80667=*> COMMIT;COMMIT
Отсутствие потерянных изменений
- В первом сеансе начните транзакцию с уровнем изоляции Repeatable Read и получите количество посылок в сортировочном центре в г. Санкт-Петербург (
hub-id = 3) с сохранением результата в переменную psql:
80642=> BEGIN ISOLATION LEVEL REPEATABLE READ;
BEGIN
80642=*> SELECT packages_amount AS spb_amount_3 FROM hubs WHERE hub_id = 3 \gset
80642=*> \echo :spb_amount_3
450
- То же самое сделайте во втором сеансе:
80667=> BEGIN ISOLATION LEVEL REPEATABLE READ;
BEGIN
80667=*> SELECT packages_amount AS spb_amount_3 FROM hubs WHERE hub_id = 3 \gset
80667=*> \echo :spb_amount_3
450
-
В первом сеансе добавьте 50 посылок к сортировочному центру в г. Санкт-Петербург (
hub_id = 3) и зафиксируйте изменения:80642=*> UPDATE hubs SET packages_amount = :spb_amount_3 + 50 WHERE hub_id = 3 RETURNING packages_amount AS spb_amount_3;spb_amount_3-----------------500(1 строка)UPDATE 180642=*> COMMIT;COMMIT -
Во втором сеансе также добавьте 50 посылок к сортировочному центру в г. Санкт-Петербург (
hub_id = 3):80667=*> UPDATE hubs SET packages_amount = :spb_amount_3 + 50 WHERE hub_id = 3 RETURNING packages_amount AS spb_amount_3;ERROR: could not serialize access due to concurrent updateВ результате получена ошибка сериализации, что предотвратило возникновение аномалии потерянных изменений. На уровне изоляции Repeatable Read разработчик должен быть готов к обработке таких ошибок с повторным выполнением транзакций.
-
Во втором сеансе откатите транзакцию:
80667=*> ROLLBACK;ROLLBACK
Несогласованная запись
-
В первом сеансе начните транзакцию с уровнем изоляции Repeatable Read и добавьте 100 посылок в сортировочный центр в г. Санкт-Петербург (
hub_id = 4) с условием того, что суммарная емкость сортировочных центров в г. Санкт-Петербург составляет 600 посылок:80642=> BEGIN ISOLATION LEVEL REPEATABLE READ;BEGIN80642=*> UPDATE hubs SET packages_amount = packages_amount + 100 WHERE hub_id = 4 RETURNING packages_amount AS spb_amount_4;spb_amount_4--------------100(1 строка)UPDATE 180642=*> SELECT * FROM hubs WHERE city = 'Санкт-Петербург';hub_id | city | packages_amount--------+-----------------+-----------------3 | Санкт-Петербург | 5004 | Санкт-Петербург | 100(2 строки) -
Во втором сеансе начните транзакцию с уровнем изоляции Repeatable Read и посмотрите загруженность сортировочных центров в г. Санкт-Петербург:
80667=> BEGIN ISOLATION LEVEL REPEATABLE READ;BEGIN80667=*> SELECT * FROM hubs WHERE city = 'Санкт-Петербург';hub_id | city | packages_amount--------+-----------------+-----------------4 | Санкт-Петербург | 03 | Санкт-Петербург | 500(2 строки)Вторая транзакция не видит изменений первой транзакции и полагает, что предельная суммарная емкость в 600 посылок еще не достигнута.
-
Во втором сеансе добавьте 100 посылок в сортировочный центр в г. Санкт-Петербург (
hub_id = 3).80667=*> UPDATE hubs SET packages_amount = packages_amount + 100 WHERE hub_id = 3 RETURNING packages_amount AS spb_amount_3;spb_amount_3--------------600(1 строка)UPDATE 180667=*> SELECT * FROM hubs WHERE city = 'Санкт-Петербург';hub_id | city | packages_amount--------+-----------------+-----------------4 | Санкт-Петербург | 03 | Санкт-Петербург | 600(2 строки) -
В первом сеансе зафиксируйте транзакцию:
80642=*> COMMIT;COMMIT -
Во втором сеансе зафиксируйте транзакцию:
80667=*> COMMIT;COMMIT -
В первом сеансе посмотрите результат:
80642=*> SELECT * FROM hubs WHERE city = 'Санкт-Петербург';hub_id | city | packages_amount--------+-----------------+-----------------4 | Санкт-Петербург | 1003 | Санкт-Петербург | 600(2 строки)В результате нарушена предельная суммарная емкость сортировочных центров в г. Санкт-Петербург. Ошибка сериализации не возникла потому, что транзакции меняли разные строки. Такая аномалия называется несогласованной записью и допускается даже на уровне изоляции Repeatable Read.
Уровень изоляции Serializable
Отсутствие аномалий
-
В первом сеансе восстановите количество посылок в сортировочных центрах г. Санкт-Петербург:
80642=> UPDATE hubs SET packages_amount = packages_amount - 100 WHERE city = 'Санкт-Петербург';UPDATE 280642=> SELECT * FROM hubs WHERE city = 'Санкт-Петербург';hub_id | city | packages_amount--------+-----------------+-----------------4 | Санкт-Петербург | 03 | Санкт-Петербург | 500(2 строки) -
В первом сеансе начните транзакцию с уровнем изоляции Serializable и добавьте 100 посылок в сортировочный центр в г. Санкт-Петербург (
hub_id = 4) с условием того, что суммарная емкость сортировочных центров в г. Санкт-Петербург составляет 600 посылок:80642=> BEGIN ISOLATION LEVEL SERIALIZABLE;BEGIN80642=*> UPDATE hubs SET packages_amount = packages_amount + 100 WHERE hub_id = 4 RETURNING packages_amount AS spb_amount_4;spb_amount_4--------------100(1 строка)UPDATE 180642=*> SELECT * FROM hubs WHERE city = 'Санкт-Петербург';hub_id | city | packages_amount--------+-----------------+-----------------3 | Санкт-Петербург | 5004 | Санкт-Петербург | 100(2 строки) -
Во втором сеансе начните транзакцию с уровнем изоляции Serializable и посмотрите загруженность сортировочных центров в г. Санкт-Петербург:
80667=> BEGIN ISOLATION LEVEL SERIALIZABLE;BEGIN80667=*> SELECT * FROM hubs WHERE city = 'Санкт-Петербург';hub_id | city | packages_amount--------+-----------------+-----------------4 | Санкт-Петербург | 03 | Санкт-Петербург | 500(2 строки)Вторая транзакция не видит изменений первой транзакции и полагает, что предельная суммарная емкость в 600 посылок еще не достигнута.
-
Во втором сеансе добавьте 100 посылок в сортировочный центр в г. Санкт-Петербург (
hub_id = 3).80667=*> UPDATE hubs SET packages_amount = packages_amount + 100 WHERE hub_id = 3 RETURNING packages_amount AS spb_amount_3;spb_amount_3--------------600(1 строка)UPDATE 180667=*> SELECT * FROM hubs WHERE city = 'Санкт-Петербург';hub_id | city | packages_amount--------+-----------------+-----------------4 | Санкт-Петербург | 03 | Санкт-Петербург | 600(2 строки)И в первой, и во второй транзакции условие предельной суммарной емкости не нарушено.
-
В первом сеансе зафиксируйте транзакцию:
80642=*> COMMIT;COMMIT -
Во втором сеансе зафиксируйте транзакцию:
80667=*> COMMIT;ERROR: could not serialize access due to read/write dependencies among transactionsПОДРОБНОСТИ: Reason code: Canceled on identification as a pivot, during commit attempt.ПОДСКАЗКА: The transaction might succeed if retried.В результате выдана ошибка сериализации. Аномалия несогласованной записи не допускается на уровне изоляции Serializable, как и любые другие аномалии. Обратите внимание, что для применения уровня изоляции Serializable параллельные транзакции должны иметь тот же уровень изоляции. В противном случае будет применяться уровень изоляции Repeatable Read, несмотря на явное указание уровня Serializable.
Завершение
-
Завершите второй сеанс psql:
80667=> \q -
В первом сеансе psql подключитесь к базе данных postgres:
80642=> \c postgresВы подключены к базе данных "postgres" как пользователь "student". -
Удалите базу данных
delivery_dbи завершите сеанс psql:135158=> drop database delivery_db;DROP DATABASE135158=> \q
Самопроверка
Вопрос 1
Открыто два сеанса psql. Первый сеанс обслуживается процессом с pid=137455, второй — процессом с pid=137503.
У каждого из сеансов в начале приглашения указывается pid обслуживающего процесса.
В указанных сеансах последовательно выполняются следующие команды:
137455=> select * from some_table;
id | amount
----+--------
1 | 10
2 | 10
(2 строки)
137455=> BEGIN ISOLATION LEVEL REPEATABLE READ;
BEGIN
137503=> BEGIN ISOLATION LEVEL READ COMMITTED;
BEGIN
137455=*> UPDATE some_table SET amount = 15 WHERE id = 1;
UPDATE 1
137455=*> COMMIT;
COMMIT
137503=*> UPDATE some_table SET amount = 15 WHERE id = 2;
UPDATE 1
137503=*> SELECT sum(amount) FROM some_table;
Какое число будет получено в результате выполнения последнего запроса?
Вопрос 2
Открыто два сеанса psql. Первый сеанс обслуживается процессом с pid=137455, второй — процессом с pid=137503.
У каждого из сеансов в начале приглашения указывается pid обслуживающего процесса.
В указанных сеансах последовательно выполняются следующие команды:
137455=> select * from some_table;
id | amount
----+--------
1 | 10
2 | 10
(2 строки)
137455=> BEGIN ISOLATION LEVEL REPEATABLE READ;
BEGIN
137503=> BEGIN ISOLATION LEVEL READ COMMITTED;
BEGIN
137503=*> INSERT INTO some_table VALUES(3, 15)
UPDATE 1
137503=*> COMMIT;
COMMIT
137455=*> DELETE FROM some_table WHERE id = 1;
137455=*> SELECT sum(amount) FROM some_table;
Какое число будет получено в результате выполнения последнего запроса?
Вопрос 3
Какие ситуации из перечисленных могут возникать при использовании транзакций с уровнем изоляции Repeatable Read?
Вопрос 4
Открыто два сеанса psql. Первый сеанс обслуживается процессом с pid=137455, второй — процессом с pid=137503.
У каждого из сеансов в начале приглашения указывается pid обслуживающего процесса.
В указанных сеансах последовательно выполняются следующие команды:
137455=> BEGIN ISOLATION LEVEL SERIALIZABLE;
BEGIN
137455=*> SELECT * FROM some_table;
id | amount
----+--------
1 | 10
2 | 10
(2 строки)
137455=*> UPDATE some_table SET amount = 15 WHERE id = 1;
UPDATE 1
137503=> BEGIN ISOLATION LEVEL REPEATABLE READ;
BEGIN
137503=*> SELECT * FROM some_table;
id | amount
----+--------
2 | 10
1 | 10
(2 строки)
137503=*> UPDATE some_table SET amount = 15 WHERE id = 2;
UPDATE 1
137503=*> COMMIT;
COMMIT
137455=*> COMMIT;
Каким будет результат выполнения последней команды?