Изоляция транзакций ~~~~~~~~~~~~~~~~~~~ psql postgres -c "CREATE DATABASE dba2_isolation;" CREATE DATABASE Откроем два сеанса, в которых одновременно будем запускать транзакции | psql dba2_isolation || psql dba2_isolation ....................................................................... Создадим таблицу с двумя строками | => create table t (id int, value int); | CREATE TABLE | => insert into t values (1, 10), (2, 20); | INSERT 0 2 | => select * from t; | id | value | ----+------- | 1 | 10 | 2 | 20 | (2 rows) | ....................................................................... Уровень изоляции: READ UNCOMMITTED ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | => begin; | BEGIN | => set transaction isolation level read uncommitted; | SET | => show transaction_isolation; | transaction_isolation | ----------------------- | read uncommitted | (1 row) | В чем дело? Ведь PostgreSQL не поддерживает чтение "грязных" данных ....................................................................... Попробуем прочитать "грязные" данные | => update t set value = 11 where id = 1; | UPDATE 1 Во втором сеансе откроем еще одну READ UNCOMMITTED транзакцию || => begin isolation level read uncommitted; || BEGIN || => select * from t where id = 1; || id | value || ----+------- || 1 | 10 || (1 row) || Вторая транзакция не видит незафиксированных изменений. Дело в том, что в PostgreSQL READ UNCOMMITTED = READ COMMITTED ....................................................................... | => rollback; | ROLLBACK Чем плохо "грязное" чтение? После отката 1-й транзакции оказалось бы, что 2-я транзакция увидела данные, которых в системе не было и нет ....................................................................... || => rollback; || ROLLBACK Уровень изоляции: READ COMMITTED ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ READ COMMITTED используется по умолчанию | => show default_transaction_isolation; | default_transaction_isolation | ------------------------------- | read committed | (1 row) | | => begin; | BEGIN | => show transaction_isolation; | transaction_isolation | ----------------------- | read committed | (1 row) | ....................................................................... Мы видели, что 2-я транзакция не видит незафиксированных изменений Посмотрим, что будет при фиксации измений | => update t set value = 11 where id = 1; | UPDATE 1 || => begin isolation level read committed; || BEGIN || => select * from t where id = 1; || id | value || ----+------- || 1 | 10 || (1 row) || | => commit; | COMMIT || => select * from t where id = 1; ....................................................................... || id | value || ----+------- || 1 | 11 || (1 row) || || => commit; || COMMIT В режиме READ COMMITTED транзакции видят зафиксированные изменения других транзакций Всё верно? ....................................................................... | => begin; | BEGIN || => begin; || BEGIN | => select * from t order by id; | id | value | ----+------- | 1 | 11 | 2 | 20 | (2 rows) | Запустим долгий запрос и пока он работает изменим таблицу во 2-м сеансе ....................................................................... | => select pg_sleep(3), sum(value) from t; || => do $$begin perform pg_sleep(1); end;$$; || DO || => update t set value = 100 where id = 2; || UPDATE 1 || => commit; || COMMIT || => select sum(value) from t; || sum || ----- || 111 || (1 row) || | pg_sleep | sum | ----------+----- | | 31 | (1 row) | | => commit; | COMMIT Если во время выполнения долгой команды другая транзакция успела зафиксировать изменения, то они не будут видны. 2-я транзакция видит только те изменения, которые были зафиксированы на момент старта очередного запроса к БД. Всё верно? ....................................................................... READ COMMITTED и lost updates | => begin; | BEGIN || => begin; || BEGIN | => select * from t where id = 1; | id | value | ----+------- | 1 | 11 | (1 row) | | => update t set value = value + 10 where id = 1; | UPDATE 1 || => update t set value = value + 10 where id = 1; Вторая транзакция ждет завершения первой ....................................................................... | => commit; | COMMIT || UPDATE 1 || => commit; || COMMIT Но какой будет результат 2-й транзакции, после фиксации в 1-й? ....................................................................... || => select * from t where id = 1; || id | value || ----+------- || 1 | 31 || (1 row) || С одной стороны, команда во 2-й транзакции не должна видеть изменения, сделанные после начала её выполнения. Но с другой стороны, она не должна потерять изменения, зафиксированные другими транзакциями. Поэтому, после снятия блокировки, она вынуждена перечитать строку, которую пытается обновить. ....................................................................... Такой результат, даже без знаний об изоляции, интуитивно кажется правильным. Но так бывает не всегда. | => create table batches (id int, status text); | CREATE TABLE | => insert into batches values (1, 'OPEN'), (2, 'CLOSED'); | INSERT 0 2 | => select * from batches; | id | status | ----+-------- | 1 | OPEN | 2 | CLOSED | (2 rows) | Можно ли быть уверенным, что после успешного выполнения следующего запроса в таблице batches не останется строк со статусом CLOSED ? delete from batches where status = 'CLOSED'; ....................................................................... | => begin isolation level read committed; | BEGIN | => update batches set status = 'CLOSED' where id = 1; | UPDATE 1 | => update batches set status = 'OPEN' where id = 2; | UPDATE 1 || => delete from batches where status = 'CLOSED'; Транзакция с командой DELETE ждет завершения 1-й транзакции. Если 1-я транзакция завершится откатом, то DELETE удалит вторую строку, что понятно и ожидаемо. ....................................................................... Но в случае успешного завершения 1-й транзакции, команда DELETE перечитает строку с id=2, убедится, что её статус не равен CLOSED и ничего не удалит. Хотя строка со статусом CLOSED присутствовала как перед выполнением DELETE, так и присутствует после выполнения. | => commit; | COMMIT || DELETE 0 || => select * from batches; || id | status || ----+-------- || 1 | CLOSED || 2 | OPEN || (2 rows) || ....................................................................... Уровень изоляции: REPEATABLE READ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Убедимся в отсутствии феномена "неповторяемое чтение" | => begin isolation level repeatable read; | BEGIN | => select * from batches; | id | status | ----+-------- | 1 | CLOSED | 2 | OPEN | (2 rows) | ....................................................................... || => update batches set status = 'CLOSED' where id = 2; || UPDATE 1 | => select * from batches; | id | status | ----+-------- | 1 | CLOSED | 2 | OPEN | (2 rows) | Повторное чтение измененной строки возвращает первоначальную строку. Это происходит потому, что снимок данных делается в начале транзакции, а не в начале команды. ....................................................................... Но если снимок делается в начале транзакции, то фантомные записи также не должны быть видны. Проверим || => insert into batches values (3, 'OPEN'); || INSERT 0 1 | => select * from batches; | id | status | ----+-------- | 1 | CLOSED | 2 | OPEN | (2 rows) | Действительно, 1-я транзакция не видит добавленной записи. Уровень изоляции REPEATABLE READ в PostgreSQL более строгий, чем того требует стандарт SQL. ....................................................................... Но нужно быть готовым к тому, если в 1-й транзакции мы захотим изменить строку, уже обновленную другой транзакцией, то мы получим ошибку: | => update batches set status = 'CLOSED' where id = 2; | ERROR: could not serialize access due to concurrent update | => rollback; | ROLLBACK ....................................................................... Уровень изоляции: SERIALIZABLE ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Транзакции могут положиться на то, что на их работу никто не повлияет | => select * from batches; | id | status | ----+-------- | 1 | CLOSED | 2 | CLOSED | 3 | OPEN | (3 rows) | | => begin isolation level serializable; | BEGIN | => update batches set status = 'OPEN' where status = 'CLOSED'; | UPDATE 2 Теперь для 1-й транзакции гарантировано, что записей со статусом CLOSED нет ....................................................................... || => begin isolation level serializable; || BEGIN || => update batches set status = 'CLOSED' where status = 'OPEN'; || UPDATE 1 А для 2-й транзакции гарантировано, что записей со статусом OPEN нет Но ведь это проблема! Теперь обе транзакции не могут успешно завершиться. Победит та, которая раньше выполнит commit ....................................................................... | => commit; | COMMIT || => commit; || ERROR: could not serialize access due to read/write dependencies among transactions || DETAIL: Reason code: Canceled on identification as a pivot, during commit attempt. || HINT: The transaction might succeed if retried. | => \q || => \q psql postgres -c "DROP DATABASE dba2_isolation;" DROP DATABASE Конец демонстрации.