Запрет на Dirty Write

Уровни изоляции касаются того, что видит транзакция. Но независимо от того, что она видит, всегда есть запрет на Dirty Write — на модификацию записи, которая параллельно изменяется другой транзакцией.

То есть если одна транзакция уже успела вызвать update или delete, то параллельная транзакция приостанавливается при попытке выполнить свой update/delete  и ждет завершения первой транзакции.

Рассмотрим пример.

Запрет на Dirty Write на практике

Пусть есть таблица PostgreSQL:

Таблица topic
Таблица topic

и две команды update, выполняющиеся в двух параллельных транзакциях. Они пытаются записать в поле title разное значение:
1-ый:

begin;
update topic set title='a'  where id=1;
select  pg_sleep(5);
commit;

2-ой скрипт:

begin;
set transacton isolation level read committed;
update topic set title='b'  where id=1;
commit;

Чтобы смоделировать параллельные транзакции, откроем две консоли и запустим скрипты в двух окнах. После update в первом окне стоит пауза 5 секунд для того, чтобы успеть запустить параллельно второй скрипт во втором окне до того момента, как первая транзакция подтвердится.

Но можно сделать по-другому — убрать строку:

select pg_sleep(5);

и вместо этого запускать команды построчно вручную — сначала в первом окне begin и update, потом переключиться на второе окно и запустить в нем весь скрипт, а потом обратно перейти на первое и подтвердить транзакцию. Это неважно. Как бы мы ни делали, второй скрипт зависнет в ожидании завершения первой транзакции. Потому что dirty write запрещен.

update/delete блокирует выполнение конкурирующих update/delete (для той же записи) до конца транзакции. Это верно при любом уровне изоляции.
В документации это отражено в абзаце (не только в Read Committed):

 

То есть сначала T1 запишет a, затем запускается T2 и видит, что строка с id=1 в процессе модификации, а значит T2 не может записать b и останавливается — ждет подтверждения либо отката T1. Когда T1 подтверждается (то есть когда через 5 сек T1 делает commit), T2 делает свой update и записывает b. Затем делает commit. Результат — b в поле title.

А что с чтением в параллельной транзакции?

Однако блокировка касается только параллельной модификации, читать запись никто не запрещает. Если во втором скрипте вместо update вызвать параллельный select, то немедленно считается старое подтвержденное значение title, транзакция не застрянет в режиме ожидания. (Можно сделать так, чтобы застряла, но для этого при чтении нужно использовать явную блокировку (select … for update)- делать так можно в крайнем случае, т.к. блокировки ухудшают производительность).

Всегда ли T2 после ожидания делает commit

Чем закончится ожидание в T2 зависит от уровня изоляции. При READ COMMITTED, как уже показано выше, T2 перезаписывает значение и транзакция подтверждается. В ней разрешено такое явление, как NON-REPEATABLE READ (неповторяемое чтение), а значит, все нормально. При уровне изоляции REPEATABLE READ транзакция T2 откатывается.

1-ый:

begin;
update topic set title='a'  where id=1;
select  pg_sleep(5);
commit;

2-ой скрипт:

begin;
set transacton isolation level repeatable read;
update topic set title='b'  where id=1;
commit;

Казалось бы, где тут вообще чтение? Просто update под капотом считывает запись (в том числе заново считывает после того, как подождет) и сравнивает с версией до начала транзакции. В случае REPEATABLE READ если update в T2 обнаруживает, что запись поменялась, T2 откатывается.

Repeatable Read
Repeatable Read

Обоснование запрета на Dirty Write

Может возникнуть вопрос, зачем нужна эта блокировка параллельного update, ведь в любом случае один update перезаписывает другой, так какая разница, какой именно. В этом примере разницы не видно, но вообще dirty write может привести к несогласованным данным.

Несогласованность данных

Например, у нас не одна, а две таблицы. И пишем мы не title, а сумму sum, причем вычитаем ее из другой суммы amount, которая во второй таблице.

В общем, эти два числа — sum и amount — как-то соотносятся. Может даже просто равны или с противоположными знаками — абсолютно неважно. И допустим, первая транзакция хочет в записать sum и amount значения -5 и +5 , а вторая -3 и +3. Но первая сначала пишет amount, потом sum, а вторая, наоборот, — сначала sum, потом amount.

Если запускать транзакции последовательно — все нормально, значения согласованны.

Запустим последовательно T1, а затем T2:

begin
amount=-3
sum=3
commit

begin 
sum=5 
amount=-5  
commit

Получается sum=5, amount=-5 — согласованные значения.

Или же при запуске T2, а затем T1:

begin 
sum=5
amount=-5 
commit

begin 
amount=-3 
sum=3 
commit

sum=3, amount=-3 —  тоже согласованные значения.

При параллельном запуске транзакций мы тоже ожидаем либо результат транзакции T1, либо T2, это было бы корректно. Но, к сожалению, если разрешить dirty write, то итоговое значение sum может быть результатом T1 (5), а значение amount — результатом T2 (-3). Короче говоря, смесь результатов двух транзакций.

Поскольку мы разрешили модификацию строки sum, которая уже в процессе модификации транзакцией T2, (а транзакция T2 еще не подтверждена), можно получить такую последовательность действий:

T1                   T2
begin                begin 
amount=-5 
                     sum=3
sum=5   
commit            
                     amount=-3
                     commit

amount последней записала вторая транзакция, а sum — первая.

В итоге получились значения sum=5, amount=-3 — не согласованны.

Проблема при откате транзакции

Еще dirty write создает проблему при откате транзакции, т.к. неясно, к какому значению откатывать.

Итоги

update и delete блокируют выполнение параллельных update и delete до завершения транзакции. Теоретические обоснования не так важны — важно понять, что в реальных базах данных  dirty write отсутствует, и обеспечивается это блокировкой, действующей до конца той транзакции, которая первая успела начать модификацию.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *