[rdbms] isolation / locking

katrin2201

Есть след простенькая транзакция

1. select count(1) into a from field where doc_id = :doc_id;
2. if (a = 0) then
3. insert into field (doc_id, value_data) values (:doc_id, 'deadbeef');
4. end if;
5. commit;

То есть проверяем, нет ли у определенного документа (:doc_id) полей, если нет, то добавляем одно поле.
Все хорошо.
Теперь пускаем две такие транзакции параллельно. Уровень изоляции у нас меньше serializable. Предположим, они обе выполняются поочередно построчно, то есть сначала первая транзакция выполнит строку 1, потом вторая строку 1, потом первая строку 2, вторая строку 2 итд.
В итоге в конце обе успешно скоммитятся и я буду иметь две строчки в таблице field для doc_id. Это плохо.
Варианты как этого избежать:
1. Serializable isolation, optimistic locking.
В этом случае в 1 строчке транзакции пометят range, но не залочат, дойдут до строчки 3, первая транзакция увидит, что она нарушит лок, и будет перезапущена (откачена?). Вторая транзакция в это время должна успеть дойти до коммита раньше, чем первая выполнит строчку 1, иначе ей тоже придется перезапуститься. Имеем livelock.
Минусы:
- если транзакции длинные(в реале они у меня оч длинные: есть внешний цикл по doc_id то перезапуск какой-нибудь транзакции будет очень дорогим
- если транзакции коротенькие, как приведено в примере выше, то оч вероятный livelock.
2. Serializable isolation, pessimistic locking.
Тут все хорошо. Транзакции в 1 строчке делают range lock, соотв вторая транзакция тут припаузится до коммита первой.
Минусы:
- собсно дорогой сам по себе pessimistic locking
3. Исправить код, добавив в 1 строчку select for update (у меня оракл). Тут то же самое, что в варианте два, только без глобального пессимистик лока. Соотв минус второго варианта уходит.
Вопрос: не лажаю ли я где-нибудь? особенно в первом варианте с лайвлоком, может сейчас делают оптимизации под этот случай (oracle 10g)? может есть еще варианты?

Ivan8209

> В итоге в конце обе успешно скоммитятся и я буду иметь две
> строчки в таблице field для doc_id. Это плохо.
alter table field add constraint doc_id_unique unique(doc_id)
---
LART: Luser Attitude Readjustment Tool.
Something large, heavy, and painful.

katrin2201

Хмых, принимается =) Плохой, слишком простой пример.
В реальной задаче есть такое понятие как "списковое поле", то есть полей у документа много, но появление дублей все равно не приветствуется.

Dasar

alter table field add constraint doc_id_unique unique(doc_id)
fail
читать надо все-таки вдумчиво, а не поверхностно (в тексте явно написано, что с одним doc_id может быть несколько значений)
вот так еще можно попробовать:
alter table field add constraint doc_id_unique unique(doc_id, value_data)
но это опять же это получится, только если совсем никогда не нужны дубли.

Ivan8209

Телепаты в отпуске, почему не подходит ограничение, не понял,
придумывай пример.
---
...Я работаю...

katrin2201

селект в 1 строчке меняем на delete всех полей у документа

Ivan8209

> читать надо все-таки вдумчиво, а не поверхностно (в тексте
> явно написано, что с одним doc_id может быть несколько значений)
В тексте явно написано, что две записи с одним doc_id --- плохо.
---
"Vyroba umelych lidi, slecno, je tovarni tajemstvi."

Ivan8209

Если это тебе важно и нужно, придумывай хороший пример.
У нас здесь даже про таблицу ничего не известно, только что там
есть два поля, причём "id" у тебя почему-то не является ключом.
---
"Vyroba umelych lidi, slecno, je tovarni tajemstvi."

katrin2201

Я это все могу описать, даже придумывать не надо, просто эта инфа будет излишней, особенно полная структура таблички fields.
Я прошу не покритиковать архитектуру моей системы, в ней есть куча мест, от которых я в ужасе. Достаточно упомянуть, что это франкенштейн из веб-сервисов, веб-приложений, и pl/sql. Причем преобладает именно pl/sql, и он не обламается дернуть какой-нибудь веб-сервис, или скачать какой-нибудь пдфник по хттп с дайджест авторизацией.
В данном случае у меня есть вполне конкретная проблема, и ее надо так или иначе решить. Менять процесс/архитектуру тут уже поздно.
В двух словах про предметную область: у нас есть документы, у документов есть поля. Мы их храним.
Документы приходят к нам извне, мы их парсим и сохраняем к себе в бд. Извне также приходят изменения документов. В этом случае документ снова распарсивается, и инфа по полям в бд обновляется.
Соответственно, ситуация моделируемая в примере - нам одновременно пришло два обновления одного и того же документа.
В описываемом плохом случае в таблице полей по данному документу все поля будут присутствовать дважды. Для какого-то документа такой набор полей может быть вполне валиден, поэтому жесткими констрейнтами это никак не описать. Но для данного документа связанные поля не будут отражать ни одно состояние, пришедшее к нам из внешней системы.

Ivan8209

> Я это все могу описать, даже придумывать не надо, просто эта
> инфа будет излишней, особенно полная структура таблички fields.
Ну так никто не просит у тебя это. Тебе сложно написать коротко?

create table f(id integer unique, val integer);
alter table f add constraint f_id_unique unique(id);
insert into f values (1, 2);
insert into f values (1, 3);

Сложно? А про предметную область это, конечно, хорошо, но,
по большому счёту, нафиг не надо.
> В двух словах про предметную область: у нас есть документы, у
> документов есть поля. Мы их храним.
> Документы приходят к нам извне, мы их парсим и сохраняем к себе в бд.
Как вы их храните? Набросай схему базы в этой части.
---
"Vyroba umelych lidi, slecno, je tovarni tajemstvi."

katrin2201

Не сложно, пожалуйста.

create table DOCS
(
ID CHAR(32) default sys_guid not null,
SRC SYS.XMLTYPE --то что пришло извне
);
alter table DOCS
add constraint PK_DOCS primary key (ID)
using index;


create table FIELDS
(
ID CHAR(32) default sys_guid not null,
ID_DOCS CHAR(32) not null,
VALUE_DATA VARCHAR2(4000
);
alter table FIELDS
add constraint PK_FIELDS primary key (ID)
using index;
alter table FIELDS
add constraint FK_FIELDS foreign key (ID_DOCS)
references DOCS (ID) on delete cascade;

Понятно, над всем этим еще есть уровень метаданных, описывающих какие типы документов бывают, какие у документов бывают поля и где их брать/как их вычислять. Приводить это здесь, думаю, излишне.

Ivan8209

Уже лучше.

alter table fields add constraint fields_attr_unique unique(id, id_docs)
-- у каждого документа может быть только один атрибут с заданным id.

Так тебя устраивает?
---
...Я работаю...

katrin2201

Нет. ID всегда генерится sys_guid'ом, никакой смысловой нагрузки не несет. Вешать на него юник констрейнт бессмысленно.
Плюс, акцентирую внимание еще раз на следующем куске текста:
Для какого-то документа такой набор полей может быть вполне валиден, поэтому жесткими констрейнтами это никак не описать.
Попытаюсь пояснить. Имеется в виду, что те "плохие" данные, которые получаются после описанной параллельной отработки транзакций могут быть вполне валидны для какого-то другого документа, но не для текущего. Соответственно, так как констрейнты в бд документы различать не могут, то разрулить это констрейнтами в бд не удастся никак.

Ivan8209

Тогда тебе надо определиться, что же ты хочешь:
то ты говоришь, что две строки плохо, то говоришь,
что оно допустимо.
А чем тебя "select ... for update" не устроил?
---
...Я работаю...

katrin2201

Я определился. Мне надо, чтобы содержимое fields соответствовало тому, что пришло извне.
Если в документе извне было два поля, то надо чтобы их было два, а не четыре. Если в документе было четыре, то надо чтобы было четыре, а не восемь. Итд.
Селект фор апдейт устраивает всем. Небольшой минус - надо, собственно, править код. Пока таких мест не очень много, соотв это достаточно дешево.
Но вдруг можно обойтись и без этого? То есть, скажем, меня бы больше устроил первый вариант, если бы там не возникало лайвлока, или он бы разруливался системой после одной-двух итераций.

Ivan8209

Нет, "select ... for update" тебя не устраивает, потому что он
"for update", а ты создавать новые записи собираешься.
Боюсь, тебе надо запирать всю таблицу.
---
...Я работаю...

katrin2201

Нет, "select ... for update" тебя не устраивает, потому что он
"for update", а ты создавать новые записи собираешься.
Вот этого я тоже боюсь.
На инсерт точно не залочит? Когда я второй раз попытаюсь такой же селект фор апдейт сделать тоже на залочит? Даже в случае Serializable? Все таки range lock и все такое...
Запирать всю таблицу вообще не вариант. В таком случае я будут писать лисопедный лок на документ.

Ivan8209

> Все таки range lock и все такое...
Как он будет работать, если у тебя id всегда новый guid?
> В таком случае я будут писать лисопедный лок на документ.
Делай "select for update" на той таблице, где unique(id_docs).
---
...Я работаю...

katrin2201

>> Все таки range lock и все такое...
> Как он будет работать, если у тебя id всегда новый guid?
Селект фор апдейт всех полей конкретного документа.
>> В таком случае я будут писать лисопедный лок на документ.
> Делай "select for update" на той таблице, где unique(id_docs).
Тоже вариант.

Ivan8209

>>> Все таки range lock и все такое...
>> Как он будет работать, если у тебя id всегда новый guid?
> Селект фор апдейт всех полей конкретного документа.
Он выберет эти записи и запрёт их, а дальше?
Те записи, которые вставлять будешь, он совсем другие будут.
Или range lock работает на внешних ключах?
---
...Я работаю...

katrin2201

Ну как, учитывая, что на уровне serializable не допустимы phantom reads, то range lock должен запирать все, что попадает под условие where, не на уровне существующих строк, а на уровне именно рейнджа. То есть, если я пытаюсь вставить новую строчку в залоченный рейндж, то меня лок должен схватить.

Ivan8209

Я тут подумал, что проще попробовать, чем гадать.
Но у меня ораклов нет (да, мне лень, но оракла всё равно нет,
так что пробуй сам).
---
"...И затем проверять то, чтобы наука у нас
не оставалась мёртвой буквой или модной фразой."

katrin2201

Да, пробовать будем по любому, просто хотелось заранее проконсультироваться =)

macrophage

сорри, что вмешиваюсь, на разве после того как все разлочится висящий инсерт, который тебе не нужен, не отработает? или я задачу не уловил?

katrin2201

Ммм. Честно говоря, я тебя не понял.
Мое утверждение в исходном посте - инсерт отрабатывает два раза, и это мне не нравится.
Повтори, пожалуйста, свое утверждение в номерах строчек и транзакций, описанных в первом посте =)

6yrop

... сначала первая транзакция выполнит строку 1, потом вторая строку 1, потом первая строку 2, вторая строку 2 итд.
...
2. Serializable isolation, pessimistic locking.
Тут все хорошо. Транзакции в 1 строчке делают range lock, соотв вторая транзакция тут припаузится до коммита первой..
не знаю как в оракле, а в MS SQL Server-е при таком сценарии, имхо, будет дедлок. Выполнив 1-ю строку, обе транзакции поставят rage lock на чтение, и будут мешать друг другу сделать инсерт.

6yrop

Минусы:
   - собсно дорогой сам по себе pessimistic locking
это не является конструктивным аргументом

katrin2201

Учитывая то, что современные dbms используют довольно навороченные алгоритмы для разрешения гораздо более сложных дедлоков, то я очень сомневаюсь, что транзакции в моем примере безысходно повиснут где бы то ни было.

katrin2201

Кому как. Мне, если я пойду этим путем, надо будет перемеривать производительность системы в целом, и с большой вероятностью полностью отказываться от этого способа. Для меня это существенно.

macrophage

Ммм. Честно говоря, я тебя не понял.
Мое утверждение в исходном посте - инсерт отрабатывает два раза, и это мне не нравится.
Повтори, пожалуйста, свое утверждение в номерах строчек и транзакций, описанных в первом посте =)

Сорри, с перваго раза не догнал, сейчас, кажется, понял, что тебе нужно.
Фор апдэйт тебе тут не поможет, во-первых, у тебя инсерты а от них таким образом не залочиться, во-вторых, у тебя count, для которого фор апдэйт ты сделать не сможешь, насколько я знаю.
Наверно выход - лочить всю таблицу.
Насколько я понимаю, задача в следующем, пока бежит 1-я транзакция, необходимо чтоб никакая другая параллельная транзакция не стартовала, ни по этому документу ни по какому-то другому, так?
----------- UPD
сорри, за повторы. облажался я... :o :crazy:
попробуй, как по ссылке, что ты привел... только мне чет пока не понятно как это работает..

6yrop

алгоритмы для разрешения гораздо более сложных дедлоков
это что такое?
дедлок можно только детектировать разрешить его никак нельзя.

6yrop

Мне, если я пойду этим путем, надо будет перемеривать производительность системы
запрос из первого поста сильно влияет на производительность системы в целом?
да и потом, перемерять полюбому придется, если ты вносишь изменение в такой критичный запрос. А вот какие ты результаты получишь это другой вопрос ;)

6yrop

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

katrin2201

mssql ставить лень, в оракле не виснет
да и я на самом деле не сомневаюсь, а уверен
dbms виснущая в описанном тобой случае - говно, а не dbms

katrin2201

пессимистичный локинг - глобальное изменение, которое может привести к совершенно неожиданным проблемам в совершенно не относящихся к делу местам
селект фор апдейт - гораздо более локальное изменение, проеврить которое проще на порядки

bansek

Я определился. Мне надо, чтобы содержимое fields соответствовало тому, что пришло извне.
а ты не можешь словить какой-н уникальный идентификатор "источника извне", чтобы дважды от него сообщения не принимать?

katrin2201

попробуй, как по ссылке, что ты привел... только мне чет пока не понятно как это работает..
ну да, на след недельке будем пробовать

katrin2201

Я обязан принимать все сообщения, ибо документы со временем меняются.

bansek

ну вот нельзя как-н хитро источник+таймстамп завязать?
потому что иначе твоя задача принципиально не решается
ты никогда не сможешь отличить легитимный апдейт от дубля

katrin2201

Ты наверное не совсем понял. Мне извне не приходят дубли. Дубли рождаются у меня в процессе обработки.
Таймстемпы меня мало волнуют, как и мало волнует был ли такой документ уже.
Единственное, что мне надо, чтобы если случайно начали обрабатываться две версии документа одновременно, то они обрабатывались "последовательно". То есть все, что мне надо - это блокировка по отдельным документам. Как ее достичь - тут и обсуждается.

bansek

во. как обычно бывает в топиках, где задача в поставленных условиях решается токо через жопу, мы добрались до постановки задачи =)
ну смотри, если у тебя один "приход" вдруг начинает обрабатываться двумя потоками, нельзя ли этот "приход" так пометить, чтобы на уровне потоков сразу просечь, что он уже обрабатывается?
я уж не говорю о том, чтобы не запускать два потока на один приход ...

Dasar

кстати да, может проще организовать единую очередь, которую будет разбирать строго один разборщик (или в целях оптимизации несколько, но работая согласованно - не беря на обработку изменение, если уже кто-то уже обрабатывает изменение с тем же doc_id)

katrin2201

Реальная постановка задачи мною действительно сознательно скрыта, потому что там много ерунды.
Я же уже писал, что меня не надо лечить, какие у меня кривые условия, непральная архитектура, надо переделать постановку, итд.
Я знаю точно, что мне надо, и хочу это минимальными изменениями достичь.
По поводу помечания приходов - ты прав, этого мне в общем то и хочется. Только у меня осложняется тем, что документ может перерасчитываться не только по приходу обновления извне, но и по иным событиям внутри уже моей системы. Соответственно, мне хочется помечать не приходы, а сами документы. Типа - он обрабатывается, ждите.
И в идеале хочется, чтобы этим "помечанием" занимались стандартные средства dbms (они вроде для этого и придуманы а не я изобретал очередной лисопедный лок, добавляя в табличку колонку с флажком "занято".

katrin2201

Вариант идеальный, но потребует слишком больших кодерских усилий, ибо события у нас разнородные и довольно сложные, продумывать их структуру ломает.

bansek

прости, я все равно не понимаю, как ты будешь отличать легитимные апдейты от не легитимных
но, если ориентироваться, на задачу из первого поста, то ultimate кувалдой, которая явно тебя спасет будет вот это: http://hashfactor.wordpress.com/2009/03/31/using-the-oracle-...
да, бтв вот здесь: книшка на 107 странице пишут, что инсерт не будет блокироваться ни на что разумное, кроме pk.
ну и еще попробуй поботать, что в случае локов будет делать merge: http://www.oracle-developer.net/display.php?id=203

6yrop

http://en.wikipedia.org/wiki/Deadlock#Avoidance
However, for many systems it is impossible to know in advance what every process will request. This means that deadlock avoidance is often impossible.

6yrop

mssql ставить лень, в оракле не виснет
да и я на самом деле не сомневаюсь, а уверен
dbms виснущая в описанном тобой случае - говно, а не dbms
на MSSQL-е вот такие два запроса попадают в дедлок
 

set transaction isolation level serializable

begin tran

declare @doc_id int
set @doc_id = 11

declare @a int

select @a = count(1) from field where doc_id = @doc_id

WAITFOR DELAY '0:0:5';

if @a = 0
insert into field (doc_id, value_data) values (@doc_id, 'deadbeef');

commit tran

Для полноты сама таблица
 

CREATE TABLE [dbo].[field](
[Id] [int] IDENTITY(1,1) NOT NULL,
[doc_id] [int] NOT NULL,
[value_data] [nvarchar](max) NOT NULL,
CONSTRAINT [PK_field] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF,
ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

Скорее всего IBM DB2 тоже выдаст дедлок в такой ситуации.
Расскажи, как ты на Oracle-е это проверял? и что он при этом делает? начинает ждать при выполнении селекта второй транзакцией?

6yrop

если поставить
set transaction isolation level snapshot  

то, как и ожидалось, вставляются две записи.

katrin2201

Нет у меня нелегитимных апдейтов. НЕ ТУ. Если родилось две транзакции на один документ - это легитимно, это надо хендлить а не запрещать.
За ссылки спасибо, кувалда явно спасет.
В книжке на 107 странице я так понимаю рассматривается дефолтная снепшот изоляция, отсюда и тезис.

katrin2201

Я где то утверждал, что эти механизмы идеальны или абсолютны?

katrin2201

На оракле гоняемся со снепшот изоляцией, соотв делает он ровно то, что описнао в примере =)

6yrop

На оракле гоняемся со снепшот изоляцией, соотв делает он ровно то, что описнао в примере =)
т.е. вставляются две записи

Id doc_id value_data
----------- ----------- ----------------------------------
13 16 deadbeef
15 17 deadbeef
16 17 deadbeef

не смотря на то, что стоит if (a = 0) ?
В первом посте у тебя написано
2. Serializable isolation, pessimistic locking.
Тут все хорошо. Транзакции в 1 строчке делают range lock, соотв вторая транзакция тут припаузится до коммита первой.

katrin2201

т.е. вставляются две записи, не смотря на то, что стоит if (a = 0) ?
Да, вставляются.
В первом посте у тебя написано
Да, написано. Пока не вижу противоречий.

6yrop

Я где то утверждал, что эти механизмы идеальны или абсолютны?
а причем тут идеальность или неидеальность алгоритмов? Ты говорил
современные dbms используют довольно навороченные алгоритмы для разрешения гораздо более сложных дедлоков, то я очень сомневаюсь, что транзакции в моем примере безысходно повиснут где бы то ни было.
...
да и я на самом деле не сомневаюсь, а уверен

под "зависнут" ты понимал именно дедлок, поскольку ты отвечал на мой пост, где я говорил о дедлоке. Однако, я тебе явно продемонстрировал, что современное СУБД-блокировщик попадает в дедлок. И вместо того, чтобы признать, что не прав в том, что в рассматриваемой ситуации случится дедлок, ты уводишь разговор в сторону.

6yrop

Да, вставляются.
и ты называешь это словами
Это плохо.

Однако, то что происходит на Serializable isolation, ты называешь
Тут все хорошо.

Т.е. ты так и не попробовал ту ситуацию на Оракле?

katrin2201


A deadlock is a situation wherein two or more competing actions are waiting for the other to finish, and thus neither ever does.
Соответственно, если dbms, после того, как два треда повиснут, увидит это, и перезапустит какой то из тредов, то уже это я называю успешным разруливанием дедлока.
Ты под этим понимаешь что-то свое, и потому докапываешься в ебанутом стиле, к сожалению, присущем этому разделу.
Пожалуйста, либо вернись к теме топика, либо заведи отдельную тему, где я с тобой с удовольствием это пообсуждаю в свободное время.

macrophage

Единственное, что мне надо, чтобы если случайноначали обрабатываться две версии документа одновременно, то ониобрабатывались "последовательно". То есть все, что мне надо - этоблокировка по отдельным документам. Как ее достичь - тут и обсуждается.

Слушай, а триггер такой написать не получится?

katrin2201

Триггер. Не знаю, пока мне не приходит ничего толкового в голову.
Триггер должен как то разбираться, кто щас инсертит поле в документ, хозяин лока или нет, а это еще отдельный наворот, соотв опять править код. Проще уж без триггера поправить код там где надо.

macrophage

Триггер. Не знаю, пока мне не приходит ничего толкового в голову.
Триггердолжен как то разбираться, кто щас инсертит поле в документ, хозяинлока или нет, а это еще отдельный наворот, соотв опять править код.Проще уж без триггера поправить код там где надо.

Думаю, если грамотно триггер написать, то и лочить ничего не надо будет, триггер будет формировать очередь для номеров сессий, которые обрабатывают разные версии одного и того же документа. Как-то так...

6yrop

перезапустит
я никак не пойму, что ты под этим подразумеваешь? Ты имеешь ввиду, что транзакция оформлена одним скриптом, который серверу отсылается за один раз? В этом случае теоретически сервер может начать транзакцию заново, но даже в этом случае MS SQL Server просто выдает указанное сообщение об ошибке. А повторять попытку или нет решает клиент. Возможно, есть режим, когда сам сервер будет повторять попытки... надо будет разузнать. Но у нас часто транзакция объединяет несколько скриптов.

katrin2201

этом случае теоретически сервер может начать транзакцию заново
Да, и для этого отнюдь не надо иметь всю транзакцию целиком в наличии.

6yrop

а до какой точки откатывать?
Клиент может еще у себя какие-то действия сделать и все в этой же транзакции.

katrin2201

Ммм. Не знаю, такое мне в голову не приходило, надо обдумать.

katrin2201

Тихо откатывать до самого начала, например, и тихо переделывать.

mbolik1

А у тебя нет некой внешней таблички documents где бы doc_id был уникальным ключом? Или её можно завести? Тогда for update можно делать на неё, что нибудь вроде:


select count(1) into a from field where doc_id = :doc_id;
if (a = 0) then
begin
select 1 into a from documents where doc_id = :doc_id for update nowait;
insert into field (doc_id, value_data) values (:doc_id, 'deadbeef');
exception when others -- лень смотреть правильный
then
null; -- Здесь можно повесить свой обработчик. У тебя сказано что ты хочешь сохранять все
-- приходящие изменения. Но если изменения не различимы, то одно можно просто выкинуть.
end;
end if;
commit;


Если нет, то можно конечно делать dbms_lock.ALLOCATE_UNIQUE(:doc_id но как бы dbms_lock не шибко быстрый, а от большого количества защищённых ресурсов вообще дохнет.
Оставить комментарий
Имя или ник:
Комментарий: