Re: Блокировки в бизнес-слое
От: Sinclair Россия https://github.com/evilguest/
Дата: 29.09.17 11:01
Оценка: 2 (1) +3
Здравствуйте, Poul_Ko, Вы писали:

P_K>Всем доброго


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


P_K>Речь идёт о задаче обеспечения неизменности состояния бизнес-объектов до конца бизнес-операции в многопользовательской среде.

P_K>Представим что есть какая-то операция, по бизнес правилам она может быть успешно выполнена только при определённом состоянии каких-то объектов. Как мы это реализуем? В коде сначала читаем эти объекты, проверяем их состояние, и если оно соответствует бизнес-правилам, то выполняем операцию. В многопользовательской среде здесь могут быть проблемы: состояние проверенных объектов может измениться между чтением объектов и выполнением операции, что по сути это приведёт к выполнению операции с нарушению бизнес-правил. Серьёзность такого нарушения может варьироваться — иногда это допустимо, а иногда нет (на одно место продано два билета).
P_K>Для избежания такой ситуации логично выполнять блокировку прочитанных объектов до конца операции так, чтобы никто другой не мог изменить их состояние.

P_K>Здесь не идёт речь о проверке состоянии самого изменяемого объекта — решение через "оптимистическую блокировку" известно. Я говорю о стабильности состояния других логически связанных сущностей, которые сами не изменяются операцией.


P_K>Первое решение, которое в принципе уже работает, это использование serializable-транзакций на базе. Здесь по сути все блокировки выполняет база. Масштабы и объём блокировок в базе в целом слабопредсказуемы, чтобы как-то на них повлиять надо плотно садиться за структуру базы и запросы.

P_K>У этого решения имеются ещё недостатки: появляются дедлоки по мере роста нагрузки и количества вовлечённых таблиц, не охватывает данные, которые не читаются из базы (находятся в кеше — справочники).

P_K>Какие другие решения можете посоветовать?

Философски — никаких других решений нет. Вы приводите пример конкурирующих изменений. Все способы решения этой проблемы давно известны.
Отличаются только детали реализации.
К примеру, "дедлок", которого вы так боитесь — это, фактически, обнаружение конфликта изменений. Его проявление в мире пессимистичных блокировок.
Какой бы способ вы ни выбрали, у вас всё равно возможна ситуация "ой, между тем, как мы показали вам цену, и вашим нажатием на кнопку Save, произошли изменения. Будете повторять?".
В pessimistic locking это будет как раз deadlock. В Optimistic locking это будет "расхождение таймстампов".
Ваше упоминание про optimistic locking связано не с проблемами OL как такового, а с его стандартной неверной реализацией.
Честная реализация OL должна трекать не только записи, но и чтения. По очевидным причинам.
Но это — очень дорого и неудобно. Поэтому вместо честного OL обычно применяют убогий. А работает он только потому, что применяется, как правило, к системам, где есть однонаправленная зависимость между данными. То есть если у нас есть конфликтующее изменение, то оно в конкурирующей транзакции обязано примениться и к нашему объекту. Ну, типа при внесении изменения в прайс-лист, в той же транзакции подправляются и все неподтверждённые заказы.
И при попытке сделать заказ подтверждённым, мы налетаем на расхождений версий заказа.

Наиболее конструктивный способ — переносить как можно больше логики в базу, и опираться на её транзакции. Чтобы время "пересечений" операций друг с другом было меньше.
Часто это решение ассоциируется со словосочетанием "логика в хранимках". Это необязательно, можно и современными средствами пользоваться, но промежуток между чтением данных и их использованием должен быть как можно короче.
В оптимистике это уменьшает количество откатов, в пессимистике уменьшает время простоя на ожидании блокировок и частоту дедлоков.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[11]: Блокировки в бизнес-слое
От: Sinclair Россия https://github.com/evilguest/
Дата: 02.10.17 09:09
Оценка: 3 (1) +1
Здравствуйте, Poul_Ko, Вы писали:
P_K>В этом конкретном случае может быть да. Но в целом когда есть проверка типа "чего-то нет" (EXISTS, COUNT) и нужно обеспечить чтобы в ходе транзакции она оставалась неизменной, то приходится использовать serializable.
Охх. Лень искать исчерпывающий пост. Кратко, тезисно:
1. Уровней изоляции не бывает. Изоляция — она либо есть, либо её нет. Для бизнеса "изоляции нет" — это смерть, т.к. это означает, что данные повреждены.
2. То, что в РСУБД называют "уровнями изоляции" — это компромисс; способ добиться сериализуемости транзакций дешевле, чем "поставив всё колом". При этом программист даёт СУБД определённые обещания.
Ну типа запрашивая repeatable read программист как бы говорит "я обещаю, что не буду перечитывать данные дважды (или вообще пользоваться данными, прочитанными "вначале", ближе к "концу" транзакции). В ответ на что СУБД говорит себе "ок, тогда я не буду удерживать shared lock до конца транзакции". Когда программист запрашивает "repeatable read", это означает, что "я не буду использовать тот факт, что некие данные отсутствовали "вначале" транзакции, ближе к "концу" транзакции (в частности, не буду полагаться на значение select count() from ... where <condition>)". На что СУБД говорит себе "а, ну, норм, можно обойтись без key range локов на запрошенные диапазоны".
3. Нарушение этих обещаний приводит к нарушению изоляции. Увы.
P_K>Да, мы выяснили, что наверняка такие проверки можно свести к проверкам значений свойств и использовать repeatable read, но это требует дополнительных телодвижений, вплоть до введения новых принципиально бесполезных для бизнеса сущностей.
Вообще все сущности для бизнеса принципиально бесполезны. Вопрос в том, как решить задачу техническими средствами, а не как сделать так, чтобы структура таблиц была похожа на графы бланка типового договора.

P_K>>>Конечно в правой части на самом деле будет не такой апдейт — там будет сначала SELECT (поиск всех неподтверждённых заказов), а потом серия апдейтов — по одному на каждый заказ. Но принципиально это ничего не меняет, даже наоборот, повышает вероятность такой логической неконсистентности.

S>>Не надо так делать.
P_K>А другого пути я не вижу. Это в примере расчёт простой — умножить одно на другое, по факту же там играет бизнес-логика. А она в коде и оперирует загруженными сущностями. То есть, пересчёт быдет выглядеть как-то так:
P_K>
P_K>// Найти заказы для пересчёта
P_K>foreach(var order = FindOrdersToRecalc(contractId))
P_K>{
P_K>    // Пересчитать каждый заказ
P_K>    _orderingService.RecalculateOrder(order);
P_K>}
P_K>

"Оперирование загруженными сущностями" свидетельствует о том, что вы рассматриваете фундаментальный вопрос про изоляцию в рамках какой-то конкретной архитектуры.
Во-первых надо понять, как вообще это всё выглядит с точки зрения СУБД. Потому что именно она разруливает то, что в итоге "сохранится для потомков". И если программист Вася перевод денег со счёта А на счёт Б сделал "в транзакции", но при этом он полагается на значения остатков на счетах, прочитанных вне этой транзакции, то виновата не СУБД и не архитектура, а лично Вася.
Васе нужно понять, что СУБД должна увидеть код вида
begin transaction
update accounts set amount=amount-@transfer_amount where ID = @source_acc_id
update accounts set amount=amount+@transfer_amount where ID = @target_acc_id
commit transaction

А за код вида
select @source_acc_amount = amount from accounts where ID = @source_acc_id
select @target_acc_amount = amount from accounts where ID = @target_acc_id
-- .... 30 секунд активности в миддл-тир ....
begin transaction
update accounts set amount=@new_source_amount where ID = @source_acc_id
update accounts set amount=@new_target_amount where ID = @target_acc_id
commit transaction

его поставят в пятую нормальную форму и будут иметь всю ночь.
Ну, то есть у Васи есть и запасные варианты — типа попытаться сделать свои механизмы изоляции снаружи СУБД. Но это — путь самурая. В основном — потому, что, как правило, он приходит в голову тем, кто не освоил использование готовой изоляцией в промышленной СУБД. Шансы, что реализация, сварганенная таким человеком, будет корректной — пренебрежимо малы.

Поэтому Васе надо сосредоточиться на том, чтобы SQL выхлоп от той прослойки, которая используется в его мидл-тире, соответствовал чертежу.
Способов у этого несколько:
1. Воспользоваться хранимыми процедурами. За идеи типа "оперировать загруженными сущностями" стоять на горохе по 40 минут за идею.
2. Оставить логику в миддл-тир, но оборудовать все чтения данных, которые будут использованы в рамках бизнес-транзакции, рукопашными блокировками (например, через sp_getapplock в случае MS SQL). Или через жосткий патчинг любимого ORM, чтобы тот сам трекал чтения
3. Оставить логику в миддл-тир, но переписать её на продвинутые средства вроде linq[2db]. Т.е. получить функциональный аналог п.1, но с нормальной декомпозицией и современным языком, а не процедурным языком образца 1993 года.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[9]: Блокировки в бизнес-слое
От: IB Австрия http://rsdn.ru
Дата: 28.09.17 07:44
Оценка: +2
Здравствуйте, Poul_Ko, Вы писали:


P_K>Теперь поменяем уровень изоляции на SERIALIZABLE. Всё заработает как надо — необходимые блокировки будет делать база. Чем это плохо — см. первое сообщение.

Не надо Serializable, достаточно добавить хинт UPDLOCK в первом SELECT-е. То есть решение, в том виде в котором вы хотите, заключается в более аккуратной работе с блокировками и запросами на уровне сиквела.
Мы уже победили, просто это еще не так заметно...
Re[4]: Блокировки в бизнес-слое
От: wildwind Россия  
Дата: 02.10.17 07:03
Оценка: +1 :)
Здравствуйте, Sinclair, Вы писали:

S>Что произойдёт в пессимистике? Одна из транзакций будет отстрелена с ошибкой, вторая — успешно завершится.

S>Что произойдёт в optimistic? Одна из транзакций успешно завершится, вторая упадёт с timestamp doesn't match или что там выкидывается в

Может показаться, что выбор между pessimistic и optimistic locking это дихотомия, но это не так. Есть и третий путь — eventual consistency (в отдельных местах). И во многих случаях это приемлемо для бизнеса.

К слову, при бумажном учете он был основным и ничего, бизнес работал.
Re[18]: Блокировки в бизнес-слое
От: Stalker. Австралия  
Дата: 03.10.17 23:11
Оценка: 66 (1)
Здравствуйте, Sinclair, Вы писали:

S>Хотелось бы убедительный пример.


Я никаких преимуществ linq так и не нашел, зато недостатков вагон и маленькая тележка. Да, в простых случаях, если простенькую бизнес-логику впихнуть в классы контекста, то можно быстро сваять приложение даже не зная что такое SQL Server. Оно само там будет внутри как-то работать благодаря EF, подходит студентам и разработчикам не знающим SQL.
А как только сложность приложения перерастает определенный уровень, то мгновенно вылазят проблемы, Во-первых, логику в контекстные файлы конечно-же не сложишь, нужен будет маппинг между ними. Научится писать запросы на linq — это по-сути выучить SQL по-новой, т.к. запросы там пишутся совершенно по-другому синтаксически. В простых случаях "для студентов" это прокатывает т.к. они вместо написания нормального запроса лепят просто циклы на циклах, а во что это превращается на стороне сервера и понятия не имеют. В сложных запросах с джойнами, группировками и прочим начинают вылезать ограничения linq, скажем там нет оператора IN, приходится часто искать обходные пути, компилятор отказывается воспринимать определеннные конструкции, скажем при несовпадении типа данных при джойне таблиц комилятор заставит сделать приведение к нужному типу, после чего радостно превратит это в CAST в where запроса убив мне индекс. Написать запрос конечно можно будет, только хранимка с нормальным SQL займет куда меньше времени и часто будет куда эффективней
Единственный недостаток хранимок что они начинают плодится под разные запросы, при изменении структуры базы их все надо будет обновлять, что менее удобно т.к. в linq соответствующие ошибки будут подсвечены компилятором
Re[5]: Блокировки в бизнес-слое
От: Sinclair Россия https://github.com/evilguest/
Дата: 03.10.17 08:23
Оценка: 6 (1)
Здравствуйте, Sharov, Вы писали:
S>Разве отстрелена? Или она(транзакция) просто встанет в очередь?
Дедлок означает, что в очереди оказался цикл. Чтобы его разорвать, надо выбрать жертву. Стандартная стратегия — отстреливать транзакцию с минимальной накопленной ценой. Это обеспечивает хоть какие-то шансы на успешное завершение: если так не делать, то может оказаться, что в воронку постоянно попадают новые транзакции, которые по очереди становятся жертвами, и коммитов не происходит вообще.
S>Или же поведение и в опт. и пес. блокировке зависит от модели изоляции.
Нет. От модели изоляции зависит то, какие локи накладываются в процессе исполнения транзакций.
S>Потому как пес. модель мне казалось эквивалентна сериализации... У меня небольшая путаница с моделью блокировок и уровнем изоляции -- зачем надо
S>париться над уровнем изоляции в случае пессимистичной блокировки?
Затем, что честный serializable требует накладывания довольно-таки параноидальных блокировок. В частности, если какая-то транзакция сделала select с неким предикатом P, то вплоть до конца транзакции нам надо "заморозить" не только изменение/удаление попавших под предикат записей, но и вставку таких записей. А это довольно сложно сделать точно, поэтому обычно СУБД идёт на компромисс.
Скажем, мы сделали
select * from people where lastname = 'Иванов' and age > 30
.
В параллельной транзакции вставляется запись ('Иванов', 29). Казалось бы, можно продолжать — она не попадает под предикат, так что результат первой транзакции не будет зависеть от того, закоммитили мы вторую или нет.
На практике СУБД не умеют делать "блокировку на предикат". Это было бы слишком дорого. Поэтому обходятся компромиссами — если есть индекс по фамилии, то заблокированы будут все операции с фамилией Иванов, независимо от возраста.
Если нету — то вообще при чтении придётся накладывать shared lock на всю таблицу. И только если нам повезло и есть индекс по (lastname, age), то мы сможем настолько аккуратно выбрать гранулярность, что вставить 29-летнего Иванова будет можно, а 40-летний будет ждать окончания первой транзакции.
Таким образом, мы мало того что теряем время на захват локов, мы ещё и уменьшаем на ровном месте степень параллелизма; даже независимые транзакции вынуждены ждать друг друга.
А вот если мы не используем прочитанные данные "во зло", то можно и не удерживать блокировки так долго — подержали range lock в процессе чтения, и тут же отпустили. Выборка ивановых, полученная таким образом, гарантированно непротиворечива — к примеру, если наша операция вставки всегда вставляет однофамильцев попарно, то подобное чтение всегда будет возвращать чётное количество записей.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[3]: Блокировки в бизнес-слое
От: Sinclair Россия https://github.com/evilguest/
Дата: 02.10.17 03:15
Оценка: 3 (1)
Здравствуйте, ·, Вы писали:
S>>К примеру, "дедлок", которого вы так боитесь — это, фактически, обнаружение конфликта изменений.
·>Ты что-то путаешь. Обычно deadlock это взаимоблокировка в результате ошибки имплементации. Скажем, классический пример — перевод денег с аккаунта A1 на аккаунт A2 при наивной имплементации возможен deadlock если аккаунты будут будут блокироваться в произвольном порядке — один пытается заблокировать A1, затем A2, а второй пытается A2, затем A1 — то они зависнут. Если же ввести отношение порядка и блокировать всегда A1,A2 — то дедлоков никаких не будет.
Это я просто очень коротко пишу. Разверну подробнее. Давайте рассмотрим ваш изолированный пример. Реальные проблемы обычно на порядок сложнее.
Итак, откуда вообще взялась проблема? Оттуда, что у нас транзакция T2 пытается захватить ресурс A1, который уже захвачен эксклюзивно транзакцией T1. То есть обе транзакции хотят внести изменения в A1 — это и есть конфликт изменений.
Что произойдёт в пессимистике? Одна из транзакций будет отстрелена с ошибкой, вторая — успешно завершится.
Что произойдёт в optimistic? Одна из транзакций успешно завершится, вторая упадёт с timestamp doesn't match или что там выкидывается в конкретном движке.
В обоих случаях придётся делать повторную попытку проводки упавшей транзакции.
Ок, предположим, мы упорядочили в пессимистике захват блокировок. Это решило проблему? Не факт. Это, возможно, решило проблему дедлоков. Но конкуренция продолжается — то есть транзакция T2 будет просто стоять в ожидании окончания T1. В итоге может оказаться так, что общий throughput не отличается — т.е. сумма "выполнение+простой+выполнение" примерно такая же, как "выполнение+откат+повторное выполнение". Всё зависит от частоты реальных дедлоков.

S>>Какой бы способ вы ни выбрали, у вас всё равно возможна ситуация "ой, между тем, как мы показали вам цену, и вашим нажатием на кнопку Save, произошли изменения. Будете повторять?".

·>Так это OL. А в PL будет просто "Не могу открыть Соглашение, т.к. кто-то сейчас проверяет Заказ связанный с этим Соглашением. Попробуйте позже." или просто песочные часики.
Вот тут есть нюанс. При лобовой реализации окажется, что исправить прайслист в принципе невозможно — потому, что не угадаешь момент, когда нет открытых заказов, ссылающихся на эту позицию.
В решении с "дедлоками" шансы завершиться успешно выше у транзакции изменения прайс-листа, т.к.она дороже, чем транзакция заказа.
·>С OL может возникнуть livelock и starvation... что тоже неприятно.
Ну вот да.
И опять же — для одной и той же стратегии блокировки могут быть разные реализации. И разные условия работы. В частности, одно дело — неинтерактивная процедура (неважно, хранимая или нет), пробегающая за секунды, а другое дело — интерактивная операция редактирования заказа, где длительность транзакции — минуты.

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

А то ведь можно легко реализовать систему, в которой один телефонный звонок замдиректору по продажам в неудачный момент повесит всю работу отдела продаж.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[11]: Блокировки в бизнес-слое
От: IB Австрия http://rsdn.ru
Дата: 28.09.17 20:16
Оценка: 1 (1)
Здравствуйте, Poul_Ko, Вы писали:


P_K>Надо, согласен. Но сложность растёт как снежный ком и это всё быстро выходит из под контроля, когда оно на уровне базы.

Если сложность растет от работы на уровне хранилища, значит вы что-то делаете не так, должно быть ровно наоборот.

P_K>Похоже что требуется архитектурное решение, делающее по сути те же блокировки что умеет СУБД, но в терминах бизнес-слоя. Надеюсь что в итоге блокировки станут более предсказуемыми и обозримыми.

Наоборот, в этом случае все вообще выйдет из под контроля, либо будет строго однопоточным и не масштабируемым. Хранилище — это единственное место где можно более-менее адекватно разрулить проблемы с синхонизацией данных.
В принципе, в каждой БД есть возможность воспользоваться механизмом блокировок для нужд прикладного слоя. Для сиквела это пара процедур sp_getapplock/sp_releaseapplock, но это ровно про то же самое, просто позволяет синхронизировать еще и сущности которые напрямую в БД не хранятся.
Мы уже победили, просто это еще не так заметно...
Re: Блокировки в бизнес-слое
От: scf  
Дата: 27.09.17 05:24
Оценка: +1
Здравствуйте, Poul_Ko, Вы писали:

P_K>Какие другие решения можете посоветовать?


LMAX disruptor архтитектура. Если вкратце, то всю информация хранится в памяти, обработчик однопоточный. Операции (забронировать билет) читаются из блокирующей очереди. Затем меняются данные в памяти, без блокировок, т.к. обработчик однопоточный. Дальше CQRS: информация о том, что поменялось, пишется в persistent queue. При рестарте приложения данные в памяти восстанавливаются путем "перепроигрывания" всех записанный событий над некоторым начальным состоянием.
Re[7]: Блокировки в бизнес-слое
От: · Великобритания  
Дата: 27.09.17 15:34
Оценка: +1
Здравствуйте, Poul_Ko, Вы писали:

P_K>Речь не об оплате, а о создании некого заказа. Цена в нём должна строго соответствовать цене в справочнике, до определённого момента в жизни заказа.

P_K>Если цена поменялась на секунду раньше — она подтянется из справочника и заказ создастся уже с новой ценой.
P_K>Если цена поменялась на секунду позже — обработчик изменения цены найдёт заказ и проапдейтит его.
P_K>Если цена поменялась в то же самое время — вот где проблема! Можем получить заказ со старой ценой, вот что нужно побороть.
Ага. Осталось уточнить что такое "время" в многопоточной системе.
Даже в нашем физическом времени благодаря Эйнштейну понятие "в то же самое время" — относительно.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[9]: Блокировки в бизнес-слое
От: · Великобритания  
Дата: 27.09.17 16:14
Оценка: +1
Здравствуйте, Poul_Ko, Вы писали:

P_K>·>Ага. Осталось уточнить что такое "время" в многопоточной системе.

P_K>Вот как раз таки за счёт блокировок и создаётся линейное время. Блокировки не дают исполняться одновременно тем операциям, которые друг другу мешают. Как в данном случае — имея какую-то блокировку мы либо сначала создадим заказ, либо обновим справочник (смотря что захватит блокировку раньше), но одновременно и то и другое никогда не произойдёт.
Нет, линейное время может существовать только если у тебя ровно одна глобальная блокировка на всё, т.е. по сути однопоточное последовательное (линейное) исполнение. В многопоточной среде другие модели, например векторные часы — т.е. не линия, а частично-упорядоченное множество.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[11]: Блокировки в бизнес-слое
От: scf  
Дата: 28.09.17 06:04
Оценка: +1
Здравствуйте, Poul_Ko, Вы писали:

P_K>У клиента одно действие — создать новый неподтверждённый заказ. Этот заказ создаётся и болтается в системе пока его не подтвердят или не отменят. И пока он болтается неподтверждённый цена в нём всегда должна строго соответствовать цене из справочника.

P_K>Вполне допустимо, что перед созданием нового неподтверждённого заказа на экране юзер видел одну цену, а после создания она стала другой.

Я бы, на месте клиента, такой юмор не оценил. Это уже вопрос к аналитикам, которые не понимают то ли чего им надо, то ли как функционирует ПО. Если цена в неподтвержденном заказе должна всегда браться из справочника — так и берите ее всегда из справочника, а не храните в заказе. Проблема решена)

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

Что касается исходного вопроса, помимо укрупнения локов, что уже посоветовали, можно лочить более избирательно. В примере с кинотеатром, serializable не так уж страшен, если ограничить его строками для конкретного зала. Или вообще завести отдельную таблицу специально под локи и захватывать их руками в хранимке.
Re[13]: Блокировки в бизнес-слое
От: scf  
Дата: 28.09.17 06:55
Оценка: +1
Здравствуйте, Poul_Ko, Вы писали:

P_K>Пример совсем из пальца — новый заказ не может быть создан, если у клиента уже есть два неподтверждённых заказа. Но ведь второй заказ может создать параллельно несколько пользователей, в итоге заказов станет больше двух, что является нарушением требований. И тут уже решение "не дублировать данные" никак не лезет.


В данном конкретном примере, если 99.5% юзеров не смогут создать третий заказ, а у 0.5% третий заказ появится, то все это переживут. Реальный мир по определению многозадачный и асинхронный и современные системы не зря предпочитают eventual consistency.

Еще пример из той же оперы — от нас часто требуют 100% сохранность введенных данных. А когда в такую систему уже инвестировано много сил и средств, оказывается, что в случае необходимости данные за последние сутки девочки могут вбить из первичных документов.

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

Возможно, все ваши проблемы просто из-за недостаточной квалификации или бесхребетности архитектора.

Можно какой-нибудь пример из реальной жизни?
Re[10]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 28.09.17 06:59
Оценка: +1
Здравствуйте, Sinix, Вы писали:

P_K>>Проверка говорит что в заказе цена старая (100500), а в прайсе — новая (100600). Что вы ответите юзерам, когда они прибегут к вам с вопросом "как так?"? Скажете "так транзакции сложились"? "Это неважно"?

S>Отвечу, что это цена на момент покупки и что она обновится при следующем отображении заказа пользователю.
То есть ваш вариант решения — пускай хранится неправильно, будем пересчитывать на каждый чих.
S>Если хочется всегда иметь актуальную цену — надо хранить в заказе не Cost, а OrderPriceId + считать стоимость заказа динамически.
Ответил на такое же предложение в соседней ветке товарщу scf. Смотрите на вопрос шире. Само создание заказа может зависеть от изменяющихся данных, тут пересчётом не решится.
S>Или хранить hash oт rowversion всех зависимых полей и пересчитывать стоимость заказа при его несовпадении. В любом случае стоимость неоплаченного заказа — штука несколько эфемерная (по определению) и пытаться натянуть на неё любые ограничения кроме eventual consistency — дело очень неблагодарное.
Выглядит страшно и сложно

P_K>>Теперь поменяем уровень изоляции на SERIALIZABLE. Всё заработает как надо — необходимые блокировки будет делать база. Чем это плохо — см. первое сообщение.

S>Даже с serializable не факт без дополнительных телодвижений. Самый простой пример — у каждой валюты своя таблица, цена считается в нескольких валютах, пока считаем цену в одной валюте, для второй обновилась запись истории.
Через блокировки я надеюсь что можно решить всё
Перед созданием заказа блокируем нафиг все изменения по ценам и валютам, по окончании — блокировку отпускаем. Если кто-то хочет поменять курс валюты — он сделает это строго после создания заказа, и все заказы пересчитаются, новые же заказы не могут быть созданы в это время — благодаря той же самой блокировке. Вроде всё красиво.
S>Ну, т.е. возвращаемся к тому, с чего начали — при изменении большинства справочников придётся неоднократно пересчитать кучу заказов. Не самое лучшее решение.
Это недостаток данного примера. Считайте что куча небольшая и пересчёт не является чем-то тяжёлым.
Brainbench transcript #6370594
Re[17]: Блокировки в бизнес-слое
От: XuMuK Россия  
Дата: 28.09.17 09:49
Оценка: +1
Здравствуйте, Poul_Ko, Вы писали:

P_K>Версия будет та же, если меняющая транзакция ещё не закоммитилась, она же идёт параллельно. И кто гарантирует что после такой проверки но перед коммитом никто не изменит договор?

Кмк, условия применения транзакций должны быть такими:
* для подтверждения договора — версия не изменилась.
* для изменения договора — договор остался в статусе неподтвержден и версия не изменилась.
В этом случае меняющая транзакция должа заканчиваться ошибкой, если договор в статусе "подтвержден" или был изменен параллельной транзакцией. Или можно увеличивать версию при подтверждении договора, тогда обе операции допустимы, если версия с начала операции не изменилась.
Re[5]: Блокировки в бизнес-слое
От: itslave СССР  
Дата: 29.09.17 12:58
Оценка: +1
Здравствуйте, Poul_Ko, Вы писали:


P_K>Каковы будут ваши варианты решения?


Это классический race condition, который невозможно разрулить без предоставления однопоточного write-access ко всей модели данных. Это может быть сделано транзакциями уровня serializable, архитектурой LMAX, юизнестранзакциями с блокировкой "всего" и другими способами.
В случае же многопоточного доступа, мы можем играться вероятностями и временем задержки обновления цен в заказах, ничего более.
Re[22]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 05.10.17 06:46
Оценка: +1
Здравствуйте, Sinclair, Вы писали:

S>Здравствуйте, Poul_Ko, Вы писали:

P_K>>Но ведь linq оперирует объектами, напрямую отражающими структуру таблиц?
S>Да.
P_K>>Предположим что у нас прайс-лист для какого-то одного случая хранится в базе в JSON-строке. Когда я пишу логику на сущностях мне пофиг — DAL достанет этот JSON и переведёт его в объекты, которыми я уже и буду оперировать. А вот когда я пишу логику на linq? Как я смогу достать из этого прайс-листа несколько нужных мне позиций запросом? Вот я о чём.
S>Во-вторых, если СУБД умеет работать с JSON, то у вас есть шанс подпилить маппер так, чтобы парсинг выполнялся на стороне СУБД.
Юмор оценил
S>В-третьих, если СУБД не умеет работать с JSON, но вы всё равно храните данные в нём, и при этом вам надо работать с отдельными позициями, то вам пора уволить архитектора, пока он вас не загнал в банкротство.
Здесь согласен. Но в жизни не всё так просто. Бывают случаи когда сначала работать с отдельными позициями не надо, а потом вдруг стало надо в каком-то одном редком случае. И возникает два пути — либо нормализовать денормализованное, либо работать как с объектами после десериализации. В случае архитектуры "на сущностях" этот выбор есть, в случае linq — увы.

S>Ок. Откуда берутся эти стратегии? Приведите реализацию какой-нибудь из этих стратегий. Пока что всё ещё непонятно, что там будет за код, и почему его трудно переписать на linq.

Реализация будет какой-то такой:
public class PricingStrategy : IPricingStrategy {
  public decimal GetPrice(Order order) {
    if (order.PaymentMethod == PaymentMethods.Method1) {
      var priceListItem = _method1PriceListDal.FindForProduct(order.ProductId);            // Выльется в SELECT ... FROM PriceList WHERE ProductId = @p1
      if (priceListItem == null) throw new Method1ProductPriceNotFound(order.ProductId);    // Либо какая-то логика, например, взять из "дефолтного" прайса
      return priceListItem.Price;
    }
  ...
  }

Для простоты здесь в заказе нет набора айтемов, всегда один продукт. Но расширить до коллекции не сложно — будет всё равно один запрос (с IN) и возвращаться будет, например, IDictionary<int, decimal>.

P_K>>А не в виде IQueriable<что-то>, что нужно ещё потом правильно сджинить. Кроме того, стратегия может бросить исключение, например, "для товара нет цены в прайс-листе", что является предсказуемой бизнес-ситуацией и обрабатывается. А вот как это выяснить для IQueriable?

S>В целом — точно так же. В идеале, "стратегия" будет просто скомпилирована в SQL, который выполнится прямо в базе. И вместо отдачи decimal, который на клиенте нужен только для того, чтобы тут же отдать его обратно в базу, вернёт expression, который можно использовать в update order set TotalCost = <>.
Вот это очень важное уточнение вы почему-то опустили в предыдущем посте, оно в корне меняет смысл.
Но сначала о другом. Чем ещё не нравится использование IQueriable в бизнес коде: "не шибко опытные" разработчики могут запросто сделать с ним что-то, что генератор запросов не переварит (.Select(x => new SomeClass(x.Prop1, x.Prop2)). То есть, сам по себе IQueriable/IEnumerable не говорит что он связан с базой, и это нужно всегда держать в голове. Это больше вопрос дисциплины и т.д., но чем меньше шансов совершить ошибку, тем лучше. Когда работа с базой лежит строго в отдельном слое, то можно неопытных туда не пускать. А когда она выставлена наружу вот так неявно, это уже становится опасно.

S>Я вам попробую ещё раз объяснить, что ключ к успеху — это

S>1. Использовать возможности СУБД для изоляции транзакций. То есть следить за тем, чтобы все три пункта выполнялись в рамках одной транзакции с подходящим уровнем изоляции.
Это уже есть сейчас в системе, от используемого подхода не зависит.
S>2. Минимизировать количество раундтрипов между СУБД и миддл-тир, потому что они увеличивают время выполнения транзакции и, соответственно, увеличивают шансы напороться на ожидание или дедлок.
Здесь полностью согласен. Описанный подход на linq действительно позволит перенести какие-то операции в БД, сделав один запрос вместо нескольких.
S>3. Традиционным ответом на эти два требования является написание хранимок. К сожалению, у хранимок есть несколько фундаментальных проблем:
Хранимки не решают первую проблему, это мы уже выяснили раньше. В остальном согласен — устаревшая монструозная технология, подходит для совсем простых задач.
S>4. Потенциальным решением для этих проблем является linq — как способ порождения SQL кода из C#-кода. В таком варианте ваш код всегда корректен, дружественен оптимизатору СУБД, и при этом его всё ещё может читать и поддерживать живой человек.
При этом код становится хитрым, за счёт оперирования какими-то отложенными запросами, и в него просачиваются ограничения СУБД. Для каких-то задач это несущественно, для каких-то — будет проблемой.

Если вам интересно — можем дальше продолжить обсуждение "как сделать Х на linq2db", я ещё вижу некоторые проблемы.
Если оперировать книжными терминами, то мне кажется что linq2db идеально подходит для паттерна transaction script, но вот со сложной domain model его не подружить.

А пока на всякий случай обозначу: считаю, что помимо озвученных вами вариантов решения (хранимки и экономия запросов) имеет право на жизнь вариант "ручных крупногранулярных блокировок", который просто исключает саму возможность ожиданий и дедлоков на уровне БД. Да, ожидание будет, просто на другом уровне, и да не всегда система будет работать максимально эффективно. Но решение выглядит наиболее простым и понятным, а это по моему мнению очень важно в enterprise-разработке.
Brainbench transcript #6370594
Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 27.09.17 03:36
Оценка:
Всем доброго

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

Речь идёт о задаче обеспечения неизменности состояния бизнес-объектов до конца бизнес-операции в многопользовательской среде.
Представим что есть какая-то операция, по бизнес правилам она может быть успешно выполнена только при определённом состоянии каких-то объектов. Как мы это реализуем? В коде сначала читаем эти объекты, проверяем их состояние, и если оно соответствует бизнес-правилам, то выполняем операцию. В многопользовательской среде здесь могут быть проблемы: состояние проверенных объектов может измениться между чтением объектов и выполнением операции, что по сути это приведёт к выполнению операции с нарушению бизнес-правил. Серьёзность такого нарушения может варьироваться — иногда это допустимо, а иногда нет (на одно место продано два билета).
Для избежания такой ситуации логично выполнять блокировку прочитанных объектов до конца операции так, чтобы никто другой не мог изменить их состояние.

Здесь не идёт речь о проверке состоянии самого изменяемого объекта — решение через "оптимистическую блокировку" известно. Я говорю о стабильности состояния других логически связанных сущностей, которые сами не изменяются операцией.

Первое решение, которое в принципе уже работает, это использование serializable-транзакций на базе. Здесь по сути все блокировки выполняет база. Масштабы и объём блокировок в базе в целом слабопредсказуемы, чтобы как-то на них повлиять надо плотно садиться за структуру базы и запросы.
У этого решения имеются ещё недостатки: появляются дедлоки по мере роста нагрузки и количества вовлечённых таблиц, не охватывает данные, которые не читаются из базы (находятся в кеше — справочники).

Какие другие решения можете посоветовать?
Писать свой менеджер блокировок? Есть примеры?

Технологии и архитектура: .NET, трёхзвенка Desktop Client — WCF services on IIS, EF — MS Sql Server, нет горизонтального масштабирования.
Brainbench transcript #6370594
Re: Блокировки в бизнес-слое
От: Sinix  
Дата: 27.09.17 06:07
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

P_K>Всем доброго


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


Кэп: решается "правильным" проектированием. Т.е. таким, чтобы место, в котором осуществляется блокировка / синхронизация была как можно меньшим, в идеале — один атомарный инкремент/обновление поля.
Не, вру. В идеале — чтобы необходимости в транзакциях не было вообще. Отличить правильное проектирование от неправильного очень просто: если команда регулярно водит хоровод вокруг кода с обсуждением "ну и что теперь со всем этим делать?" — точно неправильное

Решений может быть тонна.
В высоконагруженных системах может сработать регистрация пулов ресурсов за каждой отдельной машиной (скажем, пул регистрируется на 5 минут, каждые 30 секунд продлевается).
Это может быть установка флага бронирования на отдельном ресурсе (опять-таки с отметкой времени, когда произошло бронирование).
Это может быть try-update loop — optimistic concurrency + retry, если не получилось.
В особо отчаянных раскладах можно даже прикрутить distributed lock или реализовать обработку силами только одного потока на одной из машин (ну и держать наготове второй инстанс на подхвате).

Всё зависит от конкретных сценариев и от опыта/предпочтений в команде.
Re: Блокировки в бизнес-слое
От: Qulac Россия  
Дата: 27.09.17 08:09
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

P_K>Всем доброго


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


P_K>Речь идёт о задаче обеспечения неизменности состояния бизнес-объектов до конца бизнес-операции в многопользовательской среде.

P_K>Представим что есть какая-то операция, по бизнес правилам она может быть успешно выполнена только при определённом состоянии каких-то объектов. Как мы это реализуем? В коде сначала читаем эти объекты, проверяем их состояние, и если оно соответствует бизнес-правилам, то выполняем операцию. В многопользовательской среде здесь могут быть проблемы: состояние проверенных объектов может измениться между чтением объектов и выполнением операции, что по сути это приведёт к выполнению операции с нарушению бизнес-правил. Серьёзность такого нарушения может варьироваться — иногда это допустимо, а иногда нет (на одно место продано два билета).
P_K>Для избежания такой ситуации логично выполнять блокировку прочитанных объектов до конца операции так, чтобы никто другой не мог изменить их состояние.

P_K>Здесь не идёт речь о проверке состоянии самого изменяемого объекта — решение через "оптимистическую блокировку" известно. Я говорю о стабильности состояния других логически связанных сущностей, которые сами не изменяются операцией.


P_K>Первое решение, которое в принципе уже работает, это использование serializable-транзакций на базе. Здесь по сути все блокировки выполняет база. Масштабы и объём блокировок в базе в целом слабопредсказуемы, чтобы как-то на них повлиять надо плотно садиться за структуру базы и запросы.

P_K>У этого решения имеются ещё недостатки: появляются дедлоки по мере роста нагрузки и количества вовлечённых таблиц, не охватывает данные, которые не читаются из базы (находятся в кеше — справочники).

P_K>Какие другие решения можете посоветовать?

P_K>Писать свой менеджер блокировок? Есть примеры?

P_K>Технологии и архитектура: .NET, трёхзвенка Desktop Client — WCF services on IIS, EF — MS Sql Server, нет горизонтального масштабирования.


Смотри блокировка с низкой степенью детализации.
Программа – это мысли спрессованные в код
Re[2]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 27.09.17 09:09
Оценка:
Здравствуйте, scf, Вы писали:

scf>LMAX disruptor архтитектура. Если вкратце, то всю информация хранится в памяти, обработчик однопоточный. Операции (забронировать билет) читаются из блокирующей очереди. Затем меняются данные в памяти, без блокировок, т.к. обработчик однопоточный. Дальше CQRS: информация о том, что поменялось, пишется в persistent queue. При рестарте приложения данные в памяти восстанавливаются путем "перепроигрывания" всех записанный событий над некоторым начальным состоянием.


Спасибо, интересно.
Если я правильно понял, то суть решения в исключении источника проблемы — многопоточного исполнения. Все операции выполняются последовательно, вокруг этого накручивается некая инфраструктура, повышающая производительность.
Боюсь что для проекта в его текущем состоянии переход на такую модель потребует слишком много сил и времени.
Brainbench transcript #6370594
Re[2]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 27.09.17 09:23
Оценка:
Здравствуйте, Sinix, Вы писали:

S>Кэп: решается "правильным" проектированием. Т.е. таким, чтобы место, в котором осуществляется блокировка / синхронизация была как можно меньшим, в идеале — один атомарный инкремент/обновление поля.

Круто. Я как бэ и прошу примеры "правильно" спроектированного решения.

S>В высоконагруженных системах может сработать регистрация пулов ресурсов за каждой отдельной машиной (скажем, пул регистрируется на 5 минут, каждые 30 секунд продлевается).

Нашу систему я бы не отнёс к высоконагруженным. О каких отдельных машинах вы говорите? Всё на одном сервере, написано же в первом сообщении...

S>Это может быть установка флага бронирования на отдельном ресурсе (опять-таки с отметкой времени, когда произошло бронирование).

Что за флаг, какое у него поведение и характеристики, зачем отметка времени?

S>Это может быть try-update loop — optimistic concurrency + retry, если не получилось.

Optimistic concurrency работает в пределах изменяемого агрегата, а в моём случае проблема совсем в другом. Либо моё понимание optimistic concurrency отличается от вашего.

S>В особо отчаянных раскладах можно даже прикрутить distributed lock или реализовать обработку силами только одного потока на одной из машин (ну и держать наготове второй инстанс на подхвате).

Что это такое? Опять какая-то распределённость? Её нет у меня.

S>Всё зависит от конкретных сценариев и от опыта/предпочтений в команде.

Ясен перец Похоже наши и ваши сценарии/предпочтения очень сильно различаются.
Brainbench transcript #6370594
Re[2]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 27.09.17 09:30
Оценка:
Здравствуйте, Qulac, Вы писали:

Q>Смотри блокировка с низкой степенью детализации.


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

Известны какие-то библиотеки / референсные решения / best practices для этого?
Brainbench transcript #6370594
Re[3]: Блокировки в бизнес-слое
От: Qulac Россия  
Дата: 27.09.17 09:59
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

P_K>Здравствуйте, Qulac, Вы писали:


Q>>Смотри блокировка с низкой степенью детализации.


P_K>Дельное предложение. Если я его правильно понимаю, то суть в организации своего механизма блокировок, оперирующего не отдельными сущностями, а какими-то более общими понятиями, как бы "покрывающими" логически связанный набор сущностей. Грубо говоря, при продаже билета в кино мы блокируем весь кинозал, а не отдельное место.


Классическим примером тут является Order и OrderItem. При правке OrderItem блокируется Order и все его OderItem. Блокировка может быть как оптимистической так и пессимистической.

P_K>Известны какие-то библиотеки / референсные решения / best practices для этого?


Там ни чего сложного, см. решение у Фаулера в книге по архитектуре.
Программа – это мысли спрессованные в код
Re[4]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 27.09.17 10:22
Оценка:
Здравствуйте, Qulac, Вы писали:

Q>Классическим примером тут является Order и OrderItem. При правке OrderItem блокируется Order и все его OderItem. Блокировка может быть как оптимистической так и пессимистической.


Вот это по-моему как раз не то. Это один агрегат. Агрегат есть единица обеспечения целостности и поэтому он всегда должен обрабатываться целиком, в том числе блокироваться тем или иным способом.
Я же говорю о другом.
Например, вы создаёте заказ. Заказ для клиента, у клиента есть какой-то свой процент скидки, и соответственно в заказе вы должны на все позиции проставить эту скидку.
Сам по себе клиент не является частью агрегата заказа. Но на время операции создания заказа мы должны обеспечить неизменность процента скидки клиента. Иначе есть риск создать заказ со скидкой, которая уже не действительна (была изменена параллельно с созданием заказа).
Вопрос в том как обеспечить эту неизменность. Возьмём решение посредством блокировок. Объектом блокировки у нас будет клиент. То есть, на время создания заказа блокируем клиента, для которого создаётся заказ.

Теперь разовьём тему. Стоимость строки заказа складывается из цены и скидки клиента. Со скидкой разобрались, цены же берутся из прайс-листа. Значит на время создания заказа нужно блокировать ещё и прайс-лист (целиком — в этом смысл подхода "крупногранулярные блокировки").

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

Так что с фразой "Там ни чего сложного" я бы поспорил
Brainbench transcript #6370594
Re[5]: Блокировки в бизнес-слое
От: Qulac Россия  
Дата: 27.09.17 10:46
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

P_K>Здравствуйте, Qulac, Вы писали:


Q>>Классическим примером тут является Order и OrderItem. При правке OrderItem блокируется Order и все его OderItem. Блокировка может быть как оптимистической так и пессимистической.


P_K>Вот это по-моему как раз не то. Это один агрегат. Агрегат есть единица обеспечения целостности и поэтому он всегда должен обрабатываться целиком, в том числе блокироваться тем или иным способом.

P_K>Я же говорю о другом.
P_K>Например, вы создаёте заказ. Заказ для клиента, у клиента есть какой-то свой процент скидки, и соответственно в заказе вы должны на все позиции проставить эту скидку.
P_K>Сам по себе клиент не является частью агрегата заказа. Но на время операции создания заказа мы должны обеспечить неизменность процента скидки клиента. Иначе есть риск создать заказ со скидкой, которая уже не действительна (была изменена параллельно с созданием заказа).
P_K>Вопрос в том как обеспечить эту неизменность. Возьмём решение посредством блокировок. Объектом блокировки у нас будет клиент. То есть, на время создания заказа блокируем клиента, для которого создаётся заказ.

P_K>Теперь разовьём тему. Стоимость строки заказа складывается из цены и скидки клиента. Со скидкой разобрались, цены же берутся из прайс-листа. Значит на время создания заказа нужно блокировать ещё и прайс-лист (целиком — в этом смысл подхода "крупногранулярные блокировки").


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


P_K>Так что с фразой "Там ни чего сложного" я бы поспорил


Проблемы конкурентного доступа это естественная проблема доступа к данным. Пользователь (заказчик) должен озвучить, как ваша программа в таких случаях должна работать. Изолированность бизнес-транзакций являются частью т.з. на вашу программу. А так размышлять об абстрактных вещах — пустое дело.
Программа – это мысли спрессованные в код
Отредактировано 27.09.2017 10:49 Qulac . Предыдущая версия .
Re[6]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 27.09.17 11:51
Оценка:
Здравствуйте, Qulac, Вы писали:

P_K>>Например, вы создаёте заказ. Заказ для клиента, у клиента есть какой-то свой процент скидки, и соответственно в заказе вы должны на все позиции проставить эту скидку.

P_K>>Сам по себе клиент не является частью агрегата заказа. Но на время операции создания заказа мы должны обеспечить неизменность процента скидки клиента. Иначе есть риск создать заказ со скидкой, которая уже не действительна (была изменена параллельно с созданием заказа).

Q>Проблемы конкурентного доступа это естественная проблема доступа к данным. Пользователь (заказчик) должен озвучить, как ваша программа в таких случаях должна работать. Изолированность бизнес-транзакций являются частью т.з. на вашу программу. А так размышлять об абстрактных вещах — пустое дело.


Вашу мысль не понял.
Вернёмся к примеру. При создании заказа должна быть использована скидка клиента. Это и есть постановка задачи, и странно было если бы она звучала как-то так: "При создании заказа должна быть использована скидка клиента, но допустимо если система глюкнет и будет использована другая скидка, которая была до момента создания заказа."
Brainbench transcript #6370594
Re[3]: Блокировки в бизнес-слое
От: Sinix  
Дата: 27.09.17 13:45
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

S>>В высоконагруженных системах может сработать регистрация пулов ресурсов за каждой отдельной машиной (скажем, пул регистрируется на 5 минут, каждые 30 секунд продлевается).

P_K>Нашу систему я бы не отнёс к высоконагруженным. О каких отдельных машинах вы говорите? Всё на одном сервере, написано же в первом сообщении...

Ну, в таком случае кмк, нет смысла вообще выносить блокировки и проч как нечто заслуживающее отдельного внимания со стороны бизнес-кода.
Для подавляющего числа записей число конфликтов либо сводится к 0, либо достаточно last wins (к примеру, данные профиля пользователя или словарные записи, которые практически не правятся).

Остаются
* ресурсы, которые по определению исчерпаемы (то же бронирование номеров, к примеру)
* т.н. бизнес-транзакции (перевод с счёта на счёт, скажем)
* dumbass-требования вида последовательной нумерации заказов без дырок
Для этих редких случаев — да, имеет смысл заморочиться, набросать текущий биз-процесс в виде диаграммы, прикинуть возможные варианты гонок и уже исходя из этих знаний попробовать что-то переписать в плане архитектуры.

Общего решения нет, если есть конкретный пример — можно рассмотреть.
Re[4]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 27.09.17 14:21
Оценка:
Здравствуйте, Sinix, Вы писали:

S>Общего решения нет, если есть конкретный пример — можно рассмотреть.


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

Каковы будут ваши варианты решения?

Данный пример очень упрощён, на самом деле факторов много и варианты их влияния бывают самыми разными (например, наличие у клиента другого неподтверждённого заказа запрещает создавать новый).
Brainbench transcript #6370594
Re[5]: Блокировки в бизнес-слое
От: MozgC США http://nightcoder.livejournal.com
Дата: 27.09.17 14:34
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

P_K>Итак, есть бизнес-операция "Создать заказ". Заказ для какого-то клиента на какой-то товар из справочника ...

P_K>Каковы будут ваши варианты решения?

А чем не устраивает упомянутая оптимистическая блокировка? Перед переводом заказа в состояние "Подтвержден" проверять, что ничего не изменилось? И если изменилось, показывать пользователю обновлённую информацию и спрашивать подтверждение опять?
Re[6]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 27.09.17 14:44
Оценка:
Здравствуйте, MozgC, Вы писали:

MC>Здравствуйте, Poul_Ko, Вы писали:


P_K>>Итак, есть бизнес-операция "Создать заказ". Заказ для какого-то клиента на какой-то товар из справочника ...

P_K>>Каковы будут ваши варианты решения?

MC>А чем не устраивает упомянутая оптимистическая блокировка? Перед переводом заказа в состояние "Подтвержден" проверять, что ничего не изменилось? И если изменилось, показывать пользователю обновлённую информацию и спрашивать подтверждение опять?


Это не соответствует бизнес-требованиям, да и перевод заказа в статус "Подтверждён" мы здесь не рассматриваем.
Давайте считать что есть эдакое глобальное требование: у неподтверждённых заказов всегда размер скидки и цена должны соответствовать этим значениям из данных клиента и товара.
Обходных манёвров можно придумать много. Например, можно скидку и цену не хранить в заказе, а подтягивать из связанных сущностей каждый раз когда нужно... Но вопрос не об этом, повторюсь, пример упрощён.
Brainbench transcript #6370594
Re[5]: Блокировки в бизнес-слое
От: · Великобритания  
Дата: 27.09.17 14:45
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

P_K>Вернёмся к операции создания заказа. Как она реализована в коде: нашли клиента, нашли товар, посчитали стоимость, создали и сохранили сущность заказа.

P_K>Вроде всё хорошо, но представим что параллельно выполняется, скажем, изменение цены товара. Транзакции идут параллельно: первая прочитала товар со старой ценой и сохранила заказ с ней, вторая обновила цену в справочнике, но не увидела ещё не созданный заказ. В итоге имеем заказ со старой ценой. Бизнес-правила нарушены.
А что от чего зависит? Если у тебя заказ зависит от цены, а цена зависит от заказа — ну тут да, феерверк и бардак — как карты лягут.
Но обычно же цена товара устанавливается независимо, производителем или поставщиком. А по какой цене попадёт товар в заказ — это зависит только от конкретной ситуации. Скажем, в типичном интернет-магазине — цена товара должна быть такой, какая она была в момент генерации странички описания товара. А то юзеры будут беситься — "было указано 100ру, а купилось за 110ру! Верните мне мои кровные!!!". Зато получается, что разные клиенты в итоге могут купить тот же товар в одно и то же время по разной цене.
В таком случае делают цены неизменяемыми и добавляют цену как новую запись, делая старую "устаревшей", ордер ссылается на цену.
Вот, кстати, вроде простой случай, один сервер — но есть клиенты — и у них данные могут быть несогласованными — и получается те же проблемы, что и в распределённой системе, хочется тебе того или нет.

P_K>Каковы будут ваши варианты решения?

P_K>Данный пример очень упрощён, на самом деле факторов много и варианты их влияния бывают самыми разными (например, наличие у клиента другого неподтверждённого заказа запрещает создавать новый).
Оптимистичная блокировка же.

Как видишь — универсального решения тут нет, делать можно что угодно как угодно — зависит только от конкретных бизнес-требований.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[6]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 27.09.17 15:13
Оценка:
Здравствуйте, ·, Вы писали:

·>Здравствуйте, Poul_Ko, Вы писали:


P_K>>Вернёмся к операции создания заказа. Как она реализована в коде: нашли клиента, нашли товар, посчитали стоимость, создали и сохранили сущность заказа.

P_K>>Вроде всё хорошо, но представим что параллельно выполняется, скажем, изменение цены товара. Транзакции идут параллельно: первая прочитала товар со старой ценой и сохранила заказ с ней, вторая обновила цену в справочнике, но не увидела ещё не созданный заказ. В итоге имеем заказ со старой ценой. Бизнес-правила нарушены.
·>А что от чего зависит? Если у тебя заказ зависит от цены, а цена зависит от заказа — ну тут да, феерверк и бардак — как карты лягут.
Заказ в зависит от цены.
Цена в справочнике от заказов не зависит. При изменении цены мы должны обновить некоторые заказы, зависимостью это считать нельзя на мой взгляд. Это следствие первой зависимости, скорее даже метод её поддержания.

·>Но обычно же цена товара устанавливается независимо...

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

P_K>>Данный пример очень упрощён, на самом деле факторов много и варианты их влияния бывают самыми разными (например, наличие у клиента другого неподтверждённого заказа запрещает создавать новый).

·>Оптимистичная блокировка же.
Всё равно не пойму причём тут оптимистическая блокировка. Она решает проблему изменения состояния в долгом промежутке времени (между обращениями к серверу), а я говорю об изменении состояния за время выполнения одной операции.
На примере, мы показываем пользователю что заказ будет с ценой такой-то, он жмёт кнопку "Создать заказ", данные уходят на сервер, там проверятся соответствует ли цена в справочнике той, которая была на экране — это и есть оптимистичная блокировка. Цена не соответствует — показываем ошибку "Цена поменялась". Цена соответствует — создаём заказ. И в это время цена в справочнике меняется. Заказ создаётся со старой ценой в своей транзакции, цена в справочнике меняется в своей. В итоге бизнес-правило нарушено.
Brainbench transcript #6370594
Re[5]: Блокировки в бизнес-слое
От: Sinix  
Дата: 27.09.17 15:14
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

P_K>Бизнес-требования: при изменении скидки клиента необходимо пересчитать стоимость всех его заказов в статусе "неподтверждён". При изменении цены товара в справочнике необходимо пересчитать стоимость всех заказов в статусе "неподтверждён", в которые входит этот товар.


Покупки немного не так работают. Типовое требование (обусловленное законодательством): заказ оплачивается строго по той цене, которая была показана пользователю в момент нажатия кнопки подтвердить / пересылки на провайдер оплаты.
Изменение прайса (например, при необходимости оплатить допуслуги / доставку) — это отдельная бизнес-операция, которая также должна быть подтверждена клиентом (почтой или звонком — неважно).

Иначе никак. Цены к моменту _фактического_ поступления денег (а это ни разу не момент фиксации оплаты провайдером) могут 10 раз поменяться.
Отсюда элементарное решение: в момент фиксации заказа к нему добавляется сущность OrderPurchase, которая и хранит фактическую стоимость товара. Запись этой информации — last wins в чистом виде.
Для эстетов делаем данные справочников immutable и храним в ордере id снапшотов, а не сами id записей.

P_K>Вроде всё хорошо, но представим что параллельно выполняется, скажем, изменение цены товара. Транзакции идут параллельно: первая прочитала товар со старой ценой и сохранила заказ с ней, вторая обновила цену в справочнике, но не увидела ещё не созданный заказ. В итоге имеем заказ со старой ценой. Бизнес-правила нарушены.


Ничего страшного. Чем ситуация "стоимость поменялась за секунду до оплаты товара" отличается от "на секунду позже"?
Re[6]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 27.09.17 15:28
Оценка:
Здравствуйте, Sinix, Вы писали:

S>Покупки немного не так работают...

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

P_K>>Вроде всё хорошо, но представим что параллельно выполняется, скажем, изменение цены товара. Транзакции идут параллельно: первая прочитала товар со старой ценой и сохранила заказ с ней, вторая обновила цену в справочнике, но не увидела ещё не созданный заказ. В итоге имеем заказ со старой ценой. Бизнес-правила нарушены.


S>Ничего страшного. Чем ситуация "стоимость поменялась за секунду до оплаты товара" отличается от "на секунду позже"?

Речь не об оплате, а о создании некого заказа. Цена в нём должна строго соответствовать цене в справочнике, до определённого момента в жизни заказа.
Если цена поменялась на секунду раньше — она подтянется из справочника и заказ создастся уже с новой ценой.
Если цена поменялась на секунду позже — обработчик изменения цены найдёт заказ и проапдейтит его.
Если цена поменялась в то же самое время — вот где проблема! Можем получить заказ со старой ценой, вот что нужно побороть.
Brainbench transcript #6370594
Re[7]: Блокировки в бизнес-слое
От: · Великобритания  
Дата: 27.09.17 15:29
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

P_K>·>А что от чего зависит? Если у тебя заказ зависит от цены, а цена зависит от заказа — ну тут да, феерверк и бардак — как карты лягут.

P_K>Заказ в зависит от цены.
P_K>Цена в справочнике от заказов не зависит. При изменении цены мы должны обновить некоторые заказы, зависимостью это считать нельзя на мой взгляд. Это следствие первой зависимости, скорее даже метод её поддержания.
Ну да, это значит что некоторые заказы зависят от цены. Всё.

P_K>>>Данный пример очень упрощён, на самом деле факторов много и варианты их влияния бывают самыми разными (например, наличие у клиента другого неподтверждённого заказа запрещает создавать новый).

P_K>·>Оптимистичная блокировка же.
P_K>Всё равно не пойму причём тут оптимистическая блокировка. Она решает проблему изменения состояния в долгом промежутке времени (между обращениями к серверу), а я говорю об изменении состояния за время выполнения одной операции.
P_K>На примере, мы показываем пользователю что заказ будет с ценой такой-то, он жмёт кнопку "Создать заказ", данные уходят на сервер, там проверятся соответствует ли цена в справочнике той, которая была на экране — это и есть оптимистичная блокировка. Цена не соответствует — показываем ошибку "Цена поменялась". Цена соответствует — создаём заказ. И в это время цена в справочнике меняется. Заказ создаётся со старой ценой в своей транзакции, цена в справочнике меняется в своей. В итоге бизнес-правило нарушено.
Это же транзакция — должно быть понятие точки коммита, которая даёт OK или FAIL. Мы не "создаём" заказ (что это вообще конкретно значит?), а _атомарно_ коммитим его — с подтвержёнными на момент коммита ценами. Любые последующие изменения цены "официально" считаются, что произошли после, а значит не должны влиять на заказ. Если коммит провалился (цены поменялись до), то транзакция фейлится и, при желании, идёт на второй круг, до тех пор, пока не даст OK.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[7]: Блокировки в бизнес-слое
От: Qulac Россия  
Дата: 27.09.17 15:32
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

P_K>Здравствуйте, Qulac, Вы писали:


P_K>>>Например, вы создаёте заказ. Заказ для клиента, у клиента есть какой-то свой процент скидки, и соответственно в заказе вы должны на все позиции проставить эту скидку.

P_K>>>Сам по себе клиент не является частью агрегата заказа. Но на время операции создания заказа мы должны обеспечить неизменность процента скидки клиента. Иначе есть риск создать заказ со скидкой, которая уже не действительна (была изменена параллельно с созданием заказа).

Q>>Проблемы конкурентного доступа это естественная проблема доступа к данным. Пользователь (заказчик) должен озвучить, как ваша программа в таких случаях должна работать. Изолированность бизнес-транзакций являются частью т.з. на вашу программу. А так размышлять об абстрактных вещах — пустое дело.


P_K>Вашу мысль не понял.

P_K>Вернёмся к примеру. При создании заказа должна быть использована скидка клиента. Это и есть постановка задачи, и странно было если бы она звучала как-то так: "При создании заказа должна быть использована скидка клиента, но допустимо если система глюкнет и будет использована другая скидка, которая была до момента создания заказа."

Не один раз читал в т.з., что должна использоваться оптимистическая блокировка. В некоторых случаях приемлема только пессимистическая блокировка. Например: клиент выбрал номер в гостинице и пошел заполнять анкету, номер в это время заблокирован, поселение в него осуществить нельзя. Все эти вещи согласовываются с заказчиком.
Программа – это мысли спрессованные в код
Re[8]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 27.09.17 15:54
Оценка:
Здравствуйте, ·, Вы писали:

P_K>>На примере, мы показываем пользователю что заказ будет с ценой такой-то, он жмёт кнопку "Создать заказ", данные уходят на сервер, там проверятся соответствует ли цена в справочнике той, которая была на экране — это и есть оптимистичная блокировка. Цена не соответствует — показываем ошибку "Цена поменялась". Цена соответствует — создаём заказ. И в это время цена в справочнике меняется. Заказ создаётся со старой ценой в своей транзакции, цена в справочнике меняется в своей. В итоге бизнес-правило нарушено.

·>Это же транзакция — должно быть понятие точки коммита, которая даёт OK или FAIL. Мы не "создаём" заказ (что это вообще конкретно значит?), а _атомарно_ коммитим его — с подтвержёнными на момент коммита ценами. Если коммит провалился (цены поменялись до), то транзакция фейлится и, при желании, идёт на второй круг, до тех пор, пока не даст OK.
Не представляю как это должно выглядеть.
Алгоритм создания заказа:
1. начать транзакцию
2. найти клиента
3. найти товар
4. создать и сохранить сущность заказа
5. commit
Между 4 и 5 опять бежим в справочник и проверяем что цена не изменилась? Это ничего не изменит! Всё равно после этой проверки и до commit она может поменяться.

·> Любые последующие изменения цены "официально" считаются, что произошли после, а значит не должны влиять на заказ.

Категорически нет. Смотрите выше — изменение цены после приводит к пересчёту неподтверждённых заказов!
Brainbench transcript #6370594
Re[8]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 27.09.17 16:00
Оценка:
Здравствуйте, ·, Вы писали:

·>Ага. Осталось уточнить что такое "время" в многопоточной системе.

Вот как раз таки за счёт блокировок и создаётся линейное время. Блокировки не дают исполняться одновременно тем операциям, которые друг другу мешают. Как в данном случае — имея какую-то блокировку мы либо сначала создадим заказ, либо обновим справочник (смотря что захватит блокировку раньше), но одновременно и то и другое никогда не произойдёт.
Brainbench transcript #6370594
Re[9]: Блокировки в бизнес-слое
От: · Великобритания  
Дата: 27.09.17 16:09
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

P_K>Здравствуйте, ·, Вы писали:


P_K>>>На примере, мы показываем пользователю что заказ будет с ценой такой-то, он жмёт кнопку "Создать заказ", данные уходят на сервер, там проверятся соответствует ли цена в справочнике той, которая была на экране — это и есть оптимистичная блокировка. Цена не соответствует — показываем ошибку "Цена поменялась". Цена соответствует — создаём заказ. И в это время цена в справочнике меняется. Заказ создаётся со старой ценой в своей транзакции, цена в справочнике меняется в своей. В итоге бизнес-правило нарушено.

P_K>·>Это же транзакция — должно быть понятие точки коммита, которая даёт OK или FAIL. Мы не "создаём" заказ (что это вообще конкретно значит?), а _атомарно_ коммитим его — с подтвержёнными на момент коммита ценами. Если коммит провалился (цены поменялись до), то транзакция фейлится и, при желании, идёт на второй круг, до тех пор, пока не даст OK.
P_K>Не представляю как это должно выглядеть.
P_K>Алгоритм создания заказа:
P_K>1. начать транзакцию
P_K>2. найти клиента
P_K>3. найти товар
P_K>4. создать и сохранить сущность заказа
P_K>5. commit
P_K>Между 4 и 5 опять бежим в справочник и проверяем что цена не изменилась? Это ничего не изменит! Всё равно после этой проверки и до commit она может поменяться.
Или я тебя совсем не понимаю, или ты не ту цель пытаешься достигнуть. Цель не добиться "одновременности" (что это вообще значит?!), а создать согласованное состояние, установить причинность событий.

P_K>·> Любые последующие изменения цены "официально" считаются, что произошли после, а значит не должны влиять на заказ.

P_K>Категорически нет. Смотрите выше — изменение цены после приводит к пересчёту неподтверждённых заказов!
1. Создать неподтверждённый заказ товара по ценам из справочника.
2. Показать заказ клиенту, клиент шлёт команду "подтвердить".
3. Сверяем цены в заказе с ценами в справочнике
4.1 Цены равны — ставим статус "подтверждён"
4.2 Цены не равны — возвратить клиенту ошибку, пересчитать заказ с учётом новых цен и goto 2.

Т.е. по сути у тебя тут распределённая система из двух компонент — клиент и сервер — они и должны согласовываться.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[7]: Блокировки в бизнес-слое
От: MozgC США http://nightcoder.livejournal.com
Дата: 27.09.17 16:16
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

P_K>На примере, мы показываем пользователю что заказ будет с ценой такой-то, он жмёт кнопку "Создать заказ", данные уходят на сервер, там проверятся соответствует ли цена в справочнике той, которая была на экране — это и есть оптимистичная блокировка. Цена не соответствует — показываем ошибку "Цена поменялась". Цена соответствует — создаём заказ. И в это время цена в справочнике меняется. Заказ создаётся со старой ценой в своей транзакции, цена в справочнике меняется в своей. В итоге бизнес-правило нарушено.


Проблему можно решить на уровне БД. Например в транзакции с уровнем Repeatable Read прочитать цены из справочника (прочитанные строки из справочника заблокируются и цены не смогут поменяться другими транзакциями), сравнить, и если все ок — подтвердить заказ и закоммитить транзакцию.

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

UPDATE 
  [Order] 
SET 
  Status = Confirmed
WHERE
  OrderID = X AND
  (SELECT COUNT(*) FROM OrderItem i INNER JOIN Price p ON i.ItemID = p.ItemID AND i.Price = p.Price WHERE OrderID = X) = ItemsInOrderCount
Re[7]: Блокировки в бизнес-слое
От: Sinix  
Дата: 27.09.17 18:08
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

S>>Покупки немного не так работают...

P_K>Блин, пример искусственный, давайте думать по факту проблемы.
Не вопрос, только решение будет тоже искусственным.

P_K>Речь не об оплате, а о создании некого заказа. Цена в нём должна строго соответствовать цене в справочнике, до определённого момента в жизни заказа.

Ну так хранить в "заказе" ссылку на справочники и считать цену динамически. Если значение в справочнике регулярно меняется — хранить ссылку на запись истории справочника.
Единственное но: из опыта примерно через 3-4 итерации в том же духе проект или наворачивается, или переходит к команде, которая предпочитает чинить реальные проблемы (в том числе смягчением требований) вместо гонки за перфекционизмом.

P_K>Если цена поменялась на секунду позже — обработчик изменения цены найдёт заказ и проапдейтит его.

Даже скромная база на пару миллионов заказов и процесс обновления цены становится очень интересным занятием. Особенно если клиенту уже отправлено письмо с предыдущей ценой.

P_K>Если цена поменялась в то же самое время — вот где проблема! Можем получить заказ со старой ценой, вот что нужно побороть.

С чего бы это? Ты ж сам говоришь, что обработчик изменения цены найдёт и обновит заказ? Или обработчик синхронным сделан, чтобы веселее было?
Re[8]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 28.09.17 04:19
Оценка:
Здравствуйте, Sinix, Вы писали:

S>Здравствуйте, Poul_Ko, Вы писали:


S>>>Покупки немного не так работают...

P_K>>Блин, пример искусственный, давайте думать по факту проблемы.
S>Не вопрос, только решение будет тоже искусственным.
Точнее будет сказать что "имена действующих лиц изменены", но сюжет сохранён.

P_K>>Если цена поменялась на секунду позже — обработчик изменения цены найдёт заказ и проапдейтит его.

S>Даже скромная база на пару миллионов заказов и процесс обновления цены становится очень интересным занятием. Особенно если клиенту уже отправлено письмо с предыдущей ценой.
Никаких писем нет, не придумывайте
Обновляются не все заказы, а только те которые находятся в определённом состоянии (неподтверждённые), их относительно немного. Но к рассматриваемой проблеме это отношения не имеет.

P_K>>Речь не об оплате, а о создании некого заказа. Цена в нём должна строго соответствовать цене в справочнике, до определённого момента в жизни заказа.

S>Ну так хранить в "заказе" ссылку на справочники и считать цену динамически. Если значение в справочнике регулярно меняется — хранить ссылку на запись истории справочника.
S>Единственное но: из опыта примерно через 3-4 итерации в том же духе проект или наворачивается, или переходит к команде, которая предпочитает чинить реальные проблемы (в том числе смягчением требований) вместо гонки за перфекционизмом.
P_K>>Если цена поменялась в то же самое время — вот где проблема! Можем получить заказ со старой ценой, вот что нужно побороть.
S>С чего бы это? Ты ж сам говоришь, что обработчик изменения цены найдёт и обновит заказ? Или обработчик синхронным сделан, чтобы веселее было?
А давайте на примере, чего это мы всё словами...
  Запросы

Что мы видим на картине? Две транзакции, идущие параллельно. Параллельно — это значит что одна начинается когда другая ещё не завершилась.
Слева — создание заказа, вначале читается цена (в локальную переменную).
Справа после прочтения цены запускается транзакция на обновление цены. Она обновляет неподтверждённые заказы, но нового заказа, создаваемого в левой части она ещё не видит.
В левой части мы создаём и сохраняем заказ. Цену взяли из локальной переменной, старую.
Транзакции завершаются, обе успешно.
Проверка говорит что в заказе цена старая (100500), а в прайсе — новая (100600). Что вы ответите юзерам, когда они прибегут к вам с вопросом "как так?"? Скажете "так транзакции сложились"? "Это неважно"?

Теперь поменяем уровень изоляции на SERIALIZABLE. Всё заработает как надо — необходимые блокировки будет делать база. Чем это плохо — см. первое сообщение.

Конечно в правой части на самом деле будет не такой апдейт — там будет сначала SELECT (поиск всех неподтверждённых заказов), а потом серия апдейтов — по одному на каждый заказ. Но принципиально это ничего не меняет, даже наоборот, повышает вероятность такой логической неконсистентности.
Brainbench transcript #6370594
Re[10]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 28.09.17 04:28
Оценка:
Здравствуйте, ·, Вы писали:

·>Или я тебя совсем не понимаю, или ты не ту цель пытаешься достигнуть. Цель не добиться "одновременности" (что это вообще значит?!), а создать согласованное состояние, установить причинность событий.

Не пытаюсь добиться одновременности, а наоборот, пытаюсь от неё избавиться, так как она порождает проблемы.
Способов избавления пока вижу два — либо всегда всё жёстко делать последовательно, либо на каких-то блокировках не давать делать одновременно некоторые операции.

P_K>>·> Любые последующие изменения цены "официально" считаются, что произошли после, а значит не должны влиять на заказ.

P_K>>Категорически нет. Смотрите выше — изменение цены после приводит к пересчёту неподтверждённых заказов!
·>1. Создать неподтверждённый заказ товара по ценам из справочника.
·>2. Показать заказ клиенту, клиент шлёт команду "подтвердить".
·>3. Сверяем цены в заказе с ценами в справочнике
·>4.1 Цены равны — ставим статус "подтверждён"
·>4.2 Цены не равны — возвратить клиенту ошибку, пересчитать заказ с учётом новых цен и goto 2.
·>Т.е. по сути у тебя тут распределённая система из двух компонент — клиент и сервер — они и должны согласовываться.
Нет, это не соответствует действительности.
У клиента одно действие — создать новый неподтверждённый заказ. Этот заказ создаётся и болтается в системе пока его не подтвердят или не отменят. И пока он болтается неподтверждённый цена в нём всегда должна строго соответствовать цене из справочника.
Вполне допустимо, что перед созданием нового неподтверждённого заказа на экране юзер видел одну цену, а после создания она стала другой. Это нормальный кейс, здесь никакой оптимистической блокировки не нужно. Да даже если бы она была, это бы не изменило ситуацию в корне.
Подтверждение заказа — это другое действие клиента, там тоже могут быть похожие проблемы, но об этом сейчас не будем.
Brainbench transcript #6370594
Re[10]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 28.09.17 04:32
Оценка:
Здравствуйте, ·, Вы писали:

·>Здравствуйте, Poul_Ko, Вы писали:


P_K>>·>Ага. Осталось уточнить что такое "время" в многопоточной системе.

P_K>>Вот как раз таки за счёт блокировок и создаётся линейное время. Блокировки не дают исполняться одновременно тем операциям, которые друг другу мешают. Как в данном случае — имея какую-то блокировку мы либо сначала создадим заказ, либо обновим справочник (смотря что захватит блокировку раньше), но одновременно и то и другое никогда не произойдёт.
·>Нет, линейное время может существовать только если у тебя ровно одна глобальная блокировка на всё, т.е. по сути однопоточное последовательное (линейное) исполнение. В многопоточной среде другие модели, например векторные часы — т.е. не линия, а частично-упорядоченное множество.
С терминологией спорить не буду, хоть горшком это назовите
Ещё раз: для некоторых операций одновременности не должно быть, спор о там как это называется в викапедиях решению проблемы не поможет.
Brainbench transcript #6370594
Re[8]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 28.09.17 04:34
Оценка:
Здравствуйте, MozgC, Вы писали:

MC>Проблему можно решить на уровне БД. Например в транзакции с уровнем Repeatable Read...

Кажется вы поняли проблему!
О решении на транзакциях было в самом первом сообщении. Да, оно работает, но не совсем И имеет свои минусы.
Brainbench transcript #6370594
Re[12]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 28.09.17 06:25
Оценка:
Здравствуйте, scf, Вы писали:

scf>Здравствуйте, Poul_Ko, Вы писали:


P_K>>У клиента одно действие — создать новый неподтверждённый заказ. Этот заказ создаётся и болтается в системе пока его не подтвердят или не отменят. И пока он болтается неподтверждённый цена в нём всегда должна строго соответствовать цене из справочника.

P_K>>Вполне допустимо, что перед созданием нового неподтверждённого заказа на экране юзер видел одну цену, а после создания она стала другой.

scf>Я бы, на месте клиента, такой юмор не оценил. Это уже вопрос к аналитикам, которые не понимают то ли чего им надо, то ли как функционирует ПО.


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

scf> Если цена в неподтвержденном заказе должна всегда браться из справочника — так и берите ее всегда из справочника, а не храните в заказе. Проблема решена)

Этот вариант решения уже пройден...
Ещё раз повторю — в реальности всё озвученное нужно умножать на десять и смотреть на вопрос шире. Представьте, что сама возможность создания заказа зависит от состояния других сущностей, которые могут параллельно измениться.
Пример совсем из пальца — новый заказ не может быть создан, если у клиента уже есть два неподтверждённых заказа. Но ведь второй заказ может создать параллельно несколько пользователей, в итоге заказов станет больше двух, что является нарушением требований. И тут уже решение "не дублировать данные" никак не лезет.
Brainbench transcript #6370594
Re[9]: Блокировки в бизнес-слое
От: Sinix  
Дата: 28.09.17 06:31
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

S>>Даже скромная база на пару миллионов заказов и процесс обновления цены становится очень интересным занятием. Особенно если клиенту уже отправлено письмо с предыдущей ценой.

P_K>Никаких писем нет, не придумывайте
Ну а если появятся? Скажем, напоминания клиенту, что он забыл оплатить набранную корзину, если корзина набрана, но не оплачена в течении суток.
Ещё раз, вы гонитесь за чисто сферическими требованиями, которые в реальной жизни не нужны. И при этом принимаете решения, которые блокируют или усложняют реализацию реальных требований.

P_K>Проверка говорит что в заказе цена старая (100500), а в прайсе — новая (100600). Что вы ответите юзерам, когда они прибегут к вам с вопросом "как так?"? Скажете "так транзакции сложились"? "Это неважно"?

Отвечу, что это цена на момент покупки и что она обновится при следующем отображении заказа пользователю.
Если хочется всегда иметь актуальную цену — надо хранить в заказе не Cost, а OrderPriceId + считать стоимость заказа динамически. Или хранить hash oт rowversion всех зависимых полей и пересчитывать стоимость заказа при его несовпадении. В любом случае стоимость неоплаченного заказа — штука несколько эфемерная (по определению) и пытаться натянуть на неё любые ограничения кроме eventual consistency — дело очень неблагодарное.

P_K>Теперь поменяем уровень изоляции на SERIALIZABLE. Всё заработает как надо — необходимые блокировки будет делать база. Чем это плохо — см. первое сообщение.

Даже с serializable не факт без дополнительных телодвижений. Самый простой пример — у каждой валюты своя таблица, цена считается в нескольких валютах, пока считаем цену в одной валюте, для второй обновилась запись истории.

Ну, т.е. возвращаемся к тому, с чего начали — при изменении большинства справочников придётся неоднократно пересчитать кучу заказов. Не самое лучшее решение.
Re[9]: Блокировки в бизнес-слое
От: · Великобритания  
Дата: 28.09.17 07:34
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

P_K>Никаких писем нет, не придумывайте

P_K>Обновляются не все заказы, а только те которые находятся в определённом состоянии (неподтверждённые), их относительно немного. Но к рассматриваемой проблеме это отношения не имеет.
Дизайн хромает...

S>>С чего бы это? Ты ж сам говоришь, что обработчик изменения цены найдёт и обновит заказ? Или обработчик синхронным сделан, чтобы веселее было?

P_K>А давайте на примере, чего это мы всё словами...
P_K>
  Запросы
P_K>Image: Queries.png

P_K>Что мы видим на картине? Две транзакции, идущие параллельно. Параллельно — это значит что одна начинается когда другая ещё не завершилась.
P_K>Слева — создание заказа, вначале читается цена (в локальную переменную).
P_K>Справа после прочтения цены запускается транзакция на обновление цены. Она обновляет неподтверждённые заказы, но нового заказа, создаваемого в левой части она ещё не видит.
P_K>В левой части мы создаём и сохраняем заказ. Цену взяли из локальной переменной, старую.
P_K>Транзакции завершаются, обе успешно.
P_K>Проверка говорит что в заказе цена старая (100500), а в прайсе — новая (100600). Что вы ответите юзерам, когда они прибегут к вам с вопросом "как так?"? Скажете "так транзакции сложились"? "Это неважно"?
У тебя проблема, что одни и те же данные копируются неконтролируемо в разные места. Ну так не делай так. Я вижу два варианта:
Не копировать цену вообще, пусть "незавершенный ордер" вообще не содержит в себе копию цены — она будет просто браться каждый раз из справочника. Только в момент фиксации ордера — цену копируем.
Либо надо версировать цену или продукт (упрощёная вариация векторных часов):
SELECT Price, PriceVer FROM Products...;
INSERT INTO Orders(...) VALUES( ..., Cost, PriceVer...);

Update Products SET Price=100600, PriceVer=PriceVer+1 WHERE Id = 1;

тогда ты сможешь сразу видеть что у ордера устаревшая цена, т.е. тот у которого версия цены не совпадает с последней:
UPDATE Orders SET Cost=p.Price * Qty, PriceVer=p.PriceVer ... JOIN Products p ON p.id=o.productId WHERE (Orders.PriceVer <> p.PriceVer)

Т.е. в этом случае избавиться от неконсистентного состояния невозможно, но зато это состояние обнаруживается и фиксится.

P_K>Теперь поменяем уровень изоляции на SERIALIZABLE. Всё заработает как надо — необходимые блокировки будет делать база. Чем это плохо — см. первое сообщение.

Это понятно, но это неинтересно — система становится однопоточной, и следовательно — с линейным временем.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[9]: Блокировки в бизнес-слое
От: IB Австрия http://rsdn.ru
Дата: 28.09.17 07:36
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

P_K>О решении на транзакциях было в самом первом сообщении. Да, оно работает, но не совсем И имеет свои минусы.

Для этой проблемы другого решения нет.
просто с транзакциями надо работать аккуратнее. Во-первых, Serializable не обязательно, до него обычно не доходит. А во-вторых, объем изменений в транзакции надо минимизировать, и все будет предсказуемо, без всяких дедлоков.
Мы уже победили, просто это еще не так заметно...
Re[11]: Блокировки в бизнес-слое
От: · Великобритания  
Дата: 28.09.17 07:53
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

P_K>Здравствуйте, ·, Вы писали:


P_K>·>Или я тебя совсем не понимаю, или ты не ту цель пытаешься достигнуть. Цель не добиться "одновременности" (что это вообще значит?!), а создать согласованное состояние, установить причинность событий.

P_K>Не пытаюсь добиться одновременности, а наоборот, пытаюсь от неё избавиться, так как она порождает проблемы.
P_K>Способов избавления пока вижу два — либо всегда всё жёстко делать последовательно, либо на каких-то блокировках не давать делать одновременно некоторые операции.

P_K>>>·> Любые последующие изменения цены "официально" считаются, что произошли после, а значит не должны влиять на заказ.

P_K>>>Категорически нет. Смотрите выше — изменение цены после приводит к пересчёту неподтверждённых заказов!
P_K>·>1. Создать неподтверждённый заказ товара по ценам из справочника.
P_K>·>2. Показать заказ клиенту, клиент шлёт команду "подтвердить".
P_K>·>3. Сверяем цены в заказе с ценами в справочнике
P_K>·>4.1 Цены равны — ставим статус "подтверждён"
P_K>·>4.2 Цены не равны — возвратить клиенту ошибку, пересчитать заказ с учётом новых цен и goto 2.
P_K>·>Т.е. по сути у тебя тут распределённая система из двух компонент — клиент и сервер — они и должны согласовываться.
P_K>Нет, это не соответствует действительности.
P_K>У клиента одно действие — создать новый неподтверждённый заказ. Этот заказ создаётся и болтается в системе пока его не подтвердят или не отменят. И пока он болтается неподтверждённый цена в нём всегда должна строго соответствовать цене из справочника.
Это означает, что тебе нужно задать явную временнУю зависимость заказа от цены. Помимо того, что я упомянул уже, вариант — заблокировать продукт перед созданием ордера:
SELECT Price FROM Products WHERE Id = 1 FOR UPDATE;
...Cost = Price * Qty...
INSERT INTO Orders(...) VALUES(..., Cost, ...);

Но тут надо очень осторожно, ибо плохо продуманные блокировки — путь к deadlock.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Отредактировано 28.09.2017 7:59 · . Предыдущая версия .
Re[14]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 28.09.17 07:59
Оценка:
Здравствуйте, scf, Вы писали:
scf>Можно какой-нибудь пример из реальной жизни?
Давайте посмотрим что-то более приближенное к реальности.

Пусть те же самые заказы могут быть оплачены по договору юрлицом. Ссылка на договор указывается в заказе. Сам по себе договор является сложной сущностью, в нём заложены сложные ограничения на те товары, которые могут быть оплачены. Например, этот товар может, этот не может, а вот тот — в количестве не более 10 по данному договору (суммарно по всем сделанным заказам). Сам договор имеет период действия, и по нему могут быть оплачены только заказы, подтверждённые в этот период. Некий аналог страхового полиса в реальной жизни.
Бизнес-требование: в момент подтверждения заказа проверить все условия по договору (срок действия, ограничения по товарам), если что-то противоречит — заказ не подтверждается.
Все свойства договора — срок действия, ограничения — могут свободно редактироваться. Нельзя менять свойства так, что в итоге появятся подтверждённые заказы, противоречащие свойствам. На неподтверждённые начхать — они просто потом не подтвердятся.

Итак, мы пишем код для метода подтверждения заказа.
Первым делом вычитываем сущность договора, вторым — проверяем его срок действия и ограничения. Для проверки ограничений а-ля "не более 10 по данному договору" нужно ещё вычитать все другие заказы по этому договору на данных товар и просуммировать количество, чтобы понять сколько из 10 ещё можно.
Никто не будет отрицать, что во время всех этих проверок может быть выполнена другая операция, меняющая что-то из уже проверенного. Например, меняющая даты действия договора так, что обрабатываемый заказ в них не попадает. В этом случае мы получаем подтверждённый заказ, нарушающий условия договора.
Не будем же мы писать мелким шрифтом, как здесь предлагали, "Условия договора проверены неточно, перепроверьте ещё раз!" И перепроверять условия при отображении заказа — это тоже бред, я думаю все согласятся.

Вот вам реальная ситуация. Маловероятная, да, но реальная. Возможно да, "все переживут", но к сожалению решать это не разработчикам, а крайними окажутся именно они. Ведь требование есть, и формально ПО его не выполнило.
Brainbench transcript #6370594
Re[10]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 28.09.17 08:04
Оценка:
Здравствуйте, IB, Вы писали:

IB>Для этой проблемы другого решения нет.

IB>просто с транзакциями надо работать аккуратнее. Во-первых, Serializable не обязательно, до него обычно не доходит. А во-вторых, объем изменений в транзакции надо минимизировать, и все будет предсказуемо, без всяких дедлоков.
Надо, согласен. Но сложность растёт как снежный ком и это всё быстро выходит из под контроля, когда оно на уровне базы.
Похоже что требуется архитектурное решение, делающее по сути те же блокировки что умеет СУБД, но в терминах бизнес-слоя. Надеюсь что в итоге блокировки станут более предсказуемыми и обозримыми.
Brainbench transcript #6370594
Re[15]: Блокировки в бизнес-слое
От: · Великобритания  
Дата: 28.09.17 08:07
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

P_K>Никто не будет отрицать, что во время всех этих проверок может быть выполнена другая операция, меняющая что-то из уже проверенного. Например, меняющая даты действия договора так, что обрабатываемый заказ в них не попадает. В этом случае мы получаем подтверждённый заказ, нарушающий условия договора.

Проверяется версия договора, которая загружена. А подтверждение заказа происходит только если его версия не изменилась. Операция меняющая даты действия договора — должна так же менять и версию:
UPDATE Agreement SET startDate = 'yesterday', Version = Version+1 WHERE id = 1


SQL выпоняет такой update атомарно. Твоя цель — свести большие сложные бизнес-операции, котоыре должны выполниться атомарно к элементарной атомарной sql-операции.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[12]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 28.09.17 08:10
Оценка:
Здравствуйте, ·, Вы писали:
·>Но тут надо очень осторожно, ибо плохо продуманные блокировки — путь к deadlock.

Дык на эти грабли уже наступили!
И пути теперь видим два — углубляться в базу, аккуратничать с блокировками и всегда это держать в уме (но это всё равно не решает проблемы если какие-то данные получены не из базы).
Либо перенести блокировки с базы на уровень выше — в бизнес-слой. Вот с целью ресёча такого решения я и затеял данную тему.
Brainbench transcript #6370594
Re[11]: Блокировки в бизнес-слое
От: Sinix  
Дата: 28.09.17 08:16
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

P_K>То есть ваш вариант решения — пускай хранится неправильно, будем пересчитывать на каждый чих.

Неа, свой вариант я в самом начале привёл — храним ту цену, которую подтвердил покупатель, пока покупатель не подтвердил — вообще не храним.

P_K>Выглядит страшно и сложно

Да, разумеется. Так и задача непростая: вы храните по сути обещание (promise) и пытаетесь вместе с ним хранить актуальную стоимость. Подвох в том, что эта актуальная стоимость относится к некоторому заранее неизвестному моменту в будущем.
До некоторой стадии (заключения контракта) считать цену продажи вообще смыла нет. Всё, что можно — показать предварительную стоимость, которая может измениться в процессе финального оформления заказа (например, выбором одного из набора бонусов или добавлением индивидуальной скидки от менеджера).

P_K>Через блокировки я надеюсь что можно решить всё

Да, конечно, но с риском, что всё уткнётся в эту блокировку. Тут как всегда — или ищем компромисс, или тормозим

S>>Ну, т.е. возвращаемся к тому, с чего начали — при изменении большинства справочников придётся неоднократно пересчитать кучу заказов. Не самое лучшее решение.

P_K>Это недостаток данного примера. Считайте что куча небольшая и пересчёт не является чем-то тяжёлым.

Ну, тогда я бы сделал блокировку на уровне приложения для начала. Решение дубовое, но позволяет хоть примерно оценить общую производительность в худшем случае.
Re[16]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 28.09.17 08:22
Оценка:
Здравствуйте, ·, Вы писали:
·>Проверяется версия договора, которая загружена. А подтверждение заказа происходит только если его версия не изменилась. Операция меняющая даты действия договора — должна так же менять и версию:
·>
·>UPDATE Agreement SET startDate = 'yesterday', Version = Version+1 WHERE id = 1
·>

·>SQL выпоняет такой update атомарно. Твоя цель — свести большие сложные бизнес-операции, котоыре должны выполниться атомарно к элементарной атомарной sql-операции.

Не могу представить как это будет в реализации.
Ну вот подтверждаем мы заказ. Открыли транзакцию, прочитали договор, проверили — всё ок. Как теперь проверить что версия не изменилась? Вычитать договор из базы опять? Версия будет та же, если меняющая транзакция ещё не закоммитилась, она же идёт параллельно. И кто гарантирует что после такой проверки но перед коммитом никто не изменит договор?

Ну и когда я слышу "свести сложные бизнес-операции к элементарной sql-операции" что-то внутри меня переворачивается Бизнес-код должен решать бизнес-задачи, а не подстраиваться под возможности SQL.
Brainbench transcript #6370594
Re[12]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 28.09.17 08:30
Оценка:
Здравствуйте, Sinix, Вы писали:
S>Да, разумеется. Так и задача непростая: вы храните по сути обещание (promise)...
Пример был сильно упрощён. См. пример про подтверждение заказа и договор
Автор: Poul_Ko
Дата: 28.09.17
. Тут уже нет никакого "обещания" — мы либо считаем заказ соответствующим договору и подтверждаем, либо нет.

P_K>>Через блокировки я надеюсь что можно решить всё

S>Да, конечно, но с риском, что всё уткнётся в эту блокировку. Тут как всегда — или ищем компромисс, или тормозим
Ну вот и ищем
Brainbench transcript #6370594
Re[13]: Блокировки в бизнес-слое
От: Sinix  
Дата: 28.09.17 10:04
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

P_K>Пример был сильно упрощён. См. пример про подтверждение заказа и договор
Автор: Poul_Ko
Дата: 28.09.17
. Тут уже нет никакого "обещания" — мы либо считаем заказ соответствующим договору и подтверждаем, либо нет.

В этом случае всё элементарно. При подготовке заказа для подтверждения тем или иным способом делаем снапшот данных словарей, его и храним при подтверждении.
В 99% случаев с точки зрения бизнеса ситуация "цена поменялась за 20 минут до заключения договора" ничем не отличается от "цена поменялась через 20 минут после заключения".
В тех редких ситуациях, когда это не так, производится доппроверка перед заключением договора, которая уменьшает окно рассогласования до 2..3 минут.
В совсем отчаянных случаях коррекция цены (или расторжение договора) через отдельную бизнес-операцию.
Re[13]: Блокировки в бизнес-слое
От: · Великобритания  
Дата: 28.09.17 11:47
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

P_K>·>Но тут надо очень осторожно, ибо плохо продуманные блокировки — путь к deadlock.

P_K>Дык на эти грабли уже наступили!
P_K>И пути теперь видим два — углубляться в базу, аккуратничать с блокировками и всегда это держать в уме (но это всё равно не решает проблемы если какие-то данные получены не из базы).
P_K>Либо перенести блокировки с базы на уровень выше — в бизнес-слой. Вот с целью ресёча такого решения я и затеял данную тему.
А кто обещал, что будет легко... Не зря же изобрели lock-free и прочее.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[17]: Блокировки в бизнес-слое
От: · Великобритания  
Дата: 28.09.17 12:06
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

P_K>·>SQL выпоняет такой update атомарно. Твоя цель — свести большие сложные бизнес-операции, котоыре должны выполниться атомарно к элементарной атомарной sql-операции.

P_K>Не могу представить как это будет в реализации.
P_K>Ну вот подтверждаем мы заказ. Открыли транзакцию, прочитали договор, проверили — всё ок. Как теперь проверить что версия не изменилась? Вычитать договор из базы опять? Версия будет та же, если меняющая транзакция ещё не закоммитилась, она же идёт параллельно. И кто гарантирует что после такой проверки но перед коммитом никто не изменит договор?
Ну объяснил же уже вроде. Не просто проверка, а условная атомарная операция. Псевдокод:
SELECT Version, startDate...etc FROM Agreement WHERE id=1;
var CurrentVersion = Version;
// проверяем... долго и упорно
checkIt(startDate);
...
checkIt(etc);

// фиксируем
UPDATE Agreement SET Confirmed='YES' WHERE id=1 AND Version=CurrentVersion;
if(updatedRecords==0) rollback;// упс - облом, придётся начать сначала.

Ну и модифицирующие операции (все или как минимум те, которые могут влиять на проверки) должны изменять версию:
UPDATE Agreement SET startDate = 'yesterday', Version = Version+1 WHERE id = 1

ACID-ность UPDATE обеспечивается sql-сервером.

P_K>Ну и когда я слышу "свести сложные бизнес-операции к элементарной sql-операции" что-то внутри меня переворачивается Бизнес-код должен решать бизнес-задачи, а не подстраиваться под возможности SQL.

Я имел в виду обеспечить атомарность сложной бизнес-операции через атомарные sql-операции. Да, блокировка — способ обеспечения атомарности. Но не единственный.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Отредактировано 28.09.2017 14:05 · . Предыдущая версия .
Re[15]: Блокировки в бизнес-слое
От: scf  
Дата: 28.09.17 12:47
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

P_K>Здравствуйте, scf, Вы писали:

scf>>Можно какой-нибудь пример из реальной жизни?
P_K>Давайте посмотрим что-то более приближенное к реальности.

P_K>Пусть те же самые заказы могут быть оплачены по договору юрлицом. Ссылка на договор указывается в заказе. Сам по себе договор является сложной сущностью, в нём заложены сложные ограничения на те товары, которые могут быть оплачены. Например, этот товар может, этот не может, а вот тот — в количестве не более 10 по данному договору (суммарно по всем сделанным заказам). Сам договор имеет период действия, и по нему могут быть оплачены только заказы, подтверждённые в этот период. Некий аналог страхового полиса в реальной жизни.

P_K>Бизнес-требование: в момент подтверждения заказа проверить все условия по договору (срок действия, ограничения по товарам), если что-то противоречит — заказ не подтверждается.
P_K>Все свойства договора — срок действия, ограничения — могут свободно редактироваться. Нельзя менять свойства так, что в итоге появятся подтверждённые заказы, противоречащие свойствам. На неподтверждённые начхать — они просто потом не подтвердятся.

P_K>Итак, мы пишем код для метода подтверждения заказа.

P_K>Первым делом вычитываем сущность договора, вторым — проверяем его срок действия и ограничения. Для проверки ограничений а-ля "не более 10 по данному договору" нужно ещё вычитать все другие заказы по этому договору на данных товар и просуммировать количество, чтобы понять сколько из 10 ещё можно.
P_K>Никто не будет отрицать, что во время всех этих проверок может быть выполнена другая операция, меняющая что-то из уже проверенного. Например, меняющая даты действия договора так, что обрабатываемый заказ в них не попадает. В этом случае мы получаем подтверждённый заказ, нарушающий условия договора.
P_K>Не будем же мы писать мелким шрифтом, как здесь предлагали, "Условия договора проверены неточно, перепроверьте ещё раз!" И перепроверять условия при отображении заказа — это тоже бред, я думаю все согласятся.

P_K>Вот вам реальная ситуация. Маловероятная, да, но реальная. Возможно да, "все переживут", но к сожалению решать это не разработчикам, а крайними окажутся именно они. Ведь требование есть, и формально ПО его не выполнило.


Похоже, что для надежного решения проблемы достаточно избавиться от COUNT() в коде подтверждения заказа. К примеру, хранить купленные лимиты прямо в договоре. Тогда хватит READ_COMMITED.

Еще один вариант — вести версионность договора. При модификации создается отдельная версия (кстати, редактирование активного договора выглядит странно, обычно к нему делают аддендумы), а заказы ссылаются на ту версию договора, с которой они были созданы.

Вообще всё это выглядит как еще один довод в пользу хранения сложных бизнес-объектов в базе в виде JSON. Помимо гибкости, и быстрых выборок/обновлений, мы получаем еще атомарные операции над бизнес-объектом с минимальным кол-вом локов.
Re: Блокировки в бизнес-слое
От: wildwind Россия  
Дата: 28.09.17 13:34
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

P_K>Какие другие решения можете посоветовать?

P_K>Писать свой менеджер блокировок? Есть примеры?

Да, если это действительно оправдано. Примеры есть. Платформа 1С — там свой менеджер блокировок, в дополнение к блокировкам СУБД. Он позволяет заблокировать прикладной объект и обойтись одной блокировкой вместо множества строк в нескольких таблицах. Таким образом уменьшается количество блокировок в базе. Кроме того, возможны такие полезные вещи, как блокировка множества строк по условию. Например (если взять один из твоих примеров), у нас в таблице хранятся статусы заказов. И мы можем одной блокировкой заблокировать все неподтвержденные заказы одного клиента. Или даже группы клиентов. Если все это блокировать в базе, это будет медленно и тяжело для базы.

Но за это приходится платить дорогую цену. Все изменения бизнес объектов должны идти через этот менеждер блокировок. Например, мы не можем выполнить сложный многострочный UPDATE или DELETE, так как наш менеджер блокировок не может проверить, не относятся ли затронутые им строки к заблокированным объектам. Из-за этого сильно страдает производительность на запись.

Ну и правильно реализовать такой менеджер непросто. Например, классическая проблема: когда отпускать блокировки зависшего/отвалившегося клиента?
Отредактировано 28.09.2017 13:36 wildwind . Предыдущая версия .
Re[2]: Блокировки в бизнес-слое
От: IB Австрия http://rsdn.ru
Дата: 28.09.17 20:25
Оценка:
Здравствуйте, wildwind, Вы писали:

W>Да, если это действительно оправдано. Примеры есть. Платформа 1С — там свой менеджер блокировок, в дополнение к блокировкам СУБД.

В 1С он не от хорошей жизни. Да и образцом правильной архитектуры 1С сложно назвать.

W> Он позволяет заблокировать прикладной объект и обойтись одной блокировкой вместо множества строк в нескольких таблицах. Таким образом уменьшается количество блокировок в базе. Кроме того, возможны такие полезные вещи, как блокировка множества строк по условию. Например (если взять один из твоих примеров), у нас в таблице хранятся статусы заказов. И мы можем одной блокировкой заблокировать все неподтвержденные заказы одного клиента. Или даже группы клиентов. Если все это блокировать в базе, это будет медленно и тяжело для базы.

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

W>Но за это приходится платить дорогую цену. Все изменения бизнес объектов должны идти через этот менеждер блокировок. Например, мы не можем выполнить сложный многострочный UPDATE или DELETE, так как наш менеджер блокировок не может проверить, не относятся ли затронутые им строки к заблокированным объектам. Из-за этого сильно страдает производительность на запись.

Отдельный плюс менеджера блокировок который встроен в БД состоит в том, что о нем знают транзакции БД.
Мы уже победили, просто это еще не так заметно...
Re[18]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 29.09.17 02:26
Оценка:
Здравствуйте, XuMuK, Вы писали:
P_K>>Версия будет та же, если меняющая транзакция ещё не закоммитилась, она же идёт параллельно. И кто гарантирует что после такой проверки но перед коммитом никто не изменит договор?
XMK>Кмк, условия применения транзакций должны быть такими:
XMK>* для подтверждения договора — версия не изменилась.
Что-то я потерял мысль...
Что вы понимаете под "условия применения транзакций"? Начало транзакции знаю, коммит и роллбэк знаю. Применение — это коммит?

"Подтверждения договора" — нет такого в бизнес-процессе, имелось в виду подтверждение заказа?
Как это будет выглядеть? Давайте в псевдокоде:
BeginTransaction();
var order = FindOrder();
var contract = FindContractForOrder(order);
VerifyContractConditions(contract, order);
order.Status = Confirmed; UpdateOrder(order);
if (GetActualContractVersion(contract.Id) == contract.Version) then CommitTransaction() else RollbackTransaction();

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

XMK>* для изменения договора — договор остался в статусе неподтвержден и версия не изменилась.

XMK>В этом случае меняющая транзакция должа заканчиваться ошибкой, если договор в статусе "подтвержден" или был изменен параллельной транзакцией. Или можно увеличивать версию при подтверждении договора, тогда обе операции допустимы, если версия с начала операции не изменилась.
Стоп, у договора нет статуса, статус у заказа.
Brainbench transcript #6370594
Re[14]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 29.09.17 02:41
Оценка:
Здравствуйте, Sinix, Вы писали:
P_K>>Пример был сильно упрощён. См. пример про подтверждение заказа и договор
Автор: Poul_Ko
Дата: 28.09.17
. Тут уже нет никакого "обещания" — мы либо считаем заказ соответствующим договору и подтверждаем, либо нет.

S>В этом случае всё элементарно. При подготовке заказа для подтверждения тем или иным способом делаем снапшот данных словарей, его и храним при подтверждении.
Оно в принципе так и происходит, если я правильно понимаю вашу мысль.
В методе подтверждения вычитываются все нужные для проверки записи, они лежат в памяти, это и есть некий снэпшот, по ним работает логика. Логика отработала, заказ подтверждён, сохранён в базу, транзакция завершена, снапшот умер.
Или не то имелось в виду?

S>В 99% случаев с точки зрения бизнеса ситуация "цена поменялась за 20 минут до заключения договора" ничем не отличается от "цена поменялась через 20 минут после заключения".

По требованиям нет.
Условия меняются до подтверждения — изменённые условия будут проверены при подтверждении, заказ из-за этого может не подтвердиться — потребует дополнительного согласования в связи с изменением условия договора.
Условия меняются после подтверждения — если они противоречат уже подтверждённым заказам, то ПО отвергает изменения, юзеры должны сначала устранить проблему, например, отменить конфликтующие заказы. Но они должны это сделать явно.

S>В тех редких ситуациях, когда это не так, производится доппроверка перед заключением договора, которая уменьшает окно рассогласования до 2..3 минут.

Что такое "окно рассогласования"?

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

Это и есть отдельная бизнес-операция, со своими правилами.
При коррекции условий договора происходит проверка всех подтверждённых заказов по данному договору, если есть заказы, которые идут в разрез с новыми условиями, то коррекции не происходит, список показывается пользователю и бизнес-операция завершается. Пользователь должен исправить заказы и попытаться ещё раз изменить условия договора.
Brainbench transcript #6370594
Re[18]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 29.09.17 02:47
Оценка:
Здравствуйте, ·, Вы писали:
·>Ну объяснил же уже вроде. Не просто проверка, а условная атомарная операция. Псевдокод:
Agreement — очевидно это договор.
·>SELECT Version, startDate...etc FROM Agreement WHERE id=1;
·>var CurrentVersion = Version;
·>// проверяем... долго и упорно
·>checkIt(startDate);
·>...
·>checkIt(etc);

·>// фиксируем
// Почему  здесь Agreement? Подтверждаем-то мы заказ!
·>UPDATE Agreement SET Confirmed='YES' WHERE id=1 AND Version=CurrentVersion;
·>if(updatedRecords==0) rollback;// упс - облом, придётся начать сначала.


·>Ну и модифицирующие операции (все или как минимум те, которые могут влиять на проверки) должны изменять версию:

Это классика оптимистической блокировки, но работает она в пределах одного агрегата, а у нас здесь два (в реальности конечно больше)...
Brainbench transcript #6370594
Re[16]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 29.09.17 02:59
Оценка:
Здравствуйте, scf, Вы писали:

scf>Похоже, что для надежного решения проблемы достаточно избавиться от COUNT() в коде подтверждения заказа. К примеру, хранить купленные лимиты прямо в договоре. Тогда хватит READ_COMMITED.

Да, этим мы можем свести зависимости до двух сущностей — заказа и договора.
Проблема с количеством становится проблемой такого же порядка, как и проблема с датой действия. Да, решить её можно через READ_COMMITED.
В принципе это может быть первым шагом к решению проблемы — снизить уровни изоляции с SERIALIZABLE до READ_COMMITED, сначала выполнив замену всех подобных условий (на кол-во, существование) на проверку значения. Не уверен что такая замена всегда удастся, но возможно стоит попробовать.
Интересно бы узнать прогноз по такому решению — насколько станет легче? Станет ли сильно меньше дедлоков? Вряд ли кто-то скажет...
Ну и опять же это не решает проблемы с данными, получаемыми не из БД.

scf>Еще один вариант — вести версионность договора. При модификации создается отдельная версия (кстати, редактирование активного договора выглядит странно, обычно к нему делают аддендумы), а заказы ссылаются на ту версию договора, с которой они были созданы.

Такое решение должно быть принято бизнесом, а ему это не требуется.
Brainbench transcript #6370594
Re[12]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 29.09.17 03:17
Оценка:
Здравствуйте, IB, Вы писали:
P_K>>Надо, согласен. Но сложность растёт как снежный ком и это всё быстро выходит из под контроля, когда оно на уровне базы.
IB>Если сложность растет от работы на уровне хранилища, значит вы что-то делаете не так, должно быть ровно наоборот.
Я имел в виду когда блокировки рулятся транзакциями на уровне базы, то они, блокировки, быстро выходят из-под контроля.
Допустим всё приемлемо работает на таких блокировках. Тут появляется новая фича, в перечень проверок добавляется чтение новой сущности (а значит и блокировка). Потом добавляется ещё одна фича, потом ещё одна. И это не в одно бизнес-действие, а в несколько. В итоге проверки блокируют условно пол-базы и "привет, дедлоки!"
Выходит из-под контроля — потому что писателям бизнес-кода неизвестно чего там заблокирует база. Это отдельный вопрос, для которого наверно нужен грамотный DBA.

P_K>>Похоже что требуется архитектурное решение, делающее по сути те же блокировки что умеет СУБД, но в терминах бизнес-слоя. Надеюсь что в итоге блокировки станут более предсказуемыми и обозримыми.

IB>Наоборот, в этом случае все вообще выйдет из под контроля, либо будет строго однопоточным и не масштабируемым. Хранилище — это единственное место где можно более-менее адекватно разрулить проблемы с синхонизацией данных.
Вот тут позволю себе не согласиться. Пусть наш бизнес-код в serializable-транзакции вычитывает какую-то сущность из базы (несколько строк из пар таблиц). Что заблокирует база? Только прочитанные строки? Какие-то свои страницы (т.е. заранее непредсказуемый набор сущностей)? Всю таблицу (все сущности)? Вот я не могу ответить на этот вопрос, это надо очень глубоко закапываться в базу. То есть для меня автоматические блокировки хранилища — вещь непредсказуемая, а значит такое решение адекватным не назовёшь.
Если блокировки не автоматические, предсказуемые, то единственным аргументом против базы я вижу вопрос производительности. Зачем бегать в базу, если можно разрулить в памяти? Масштабируемость (несколько app-серверов) на требуется.

IB>В принципе, в каждой БД есть возможность воспользоваться механизмом блокировок для нужд прикладного слоя. Для сиквела это пара процедур sp_getapplock/sp_releaseapplock, но это ровно про то же самое, просто позволяет синхронизировать еще и сущности которые напрямую в БД не хранятся.

Интересно, не знал, спасибо.
Brainbench transcript #6370594
Re[15]: Блокировки в бизнес-слое
От: Sinix  
Дата: 29.09.17 04:55
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

P_K>Или не то имелось в виду?

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

P_K>Условия меняются после подтверждения — если они противоречат уже подтверждённым заказам, то ПО отвергает изменения, юзеры должны сначала устранить проблему, например, отменить конфликтующие заказы. Но они должны это сделать явно.

Вот это не сработает. Нельзя "отменить конфликтующий заказ" простым велением левой ноги разработчика. Или такой биз-процесс обоснован реальными нуждами бизнеса и описан в ТЗ, или тот, кто занимался анализом требований схалтурил
А если онобоснован, то в 99% случаев там будет или неявное согласие с обновлённой офертой (как меняют договоры большинство провайдеров), или заключение допсоглашения (явное подтверждение изменений заказа).

S>>В тех редких ситуациях, когда это не так, производится доппроверка перед заключением договора, которая уменьшает окно рассогласования до 2..3 минут.

P_K>Что такое "окно рассогласования"?
Грубо говоря — ожидаемое время между "получили данные" и "сохранили обратно". Т.е. проверять, при неудаче сохранения (что-то поменялось) показать обновлённые данные пользователю и так по новой.

P_K>При коррекции условий договора происходит проверка всех подтверждённых заказов по данному договору, если есть заказы, которые идут в разрез с новыми условиями, то коррекции не происходит, список показывается пользователю и бизнес-операция завершается. Пользователь должен исправить заказы и попытаться ещё раз изменить условия договора.

Ну, раньше про "показывается пользователю" речи не шло, речь шла про блокировки.
В случае "показывается пользователю" блокировки не катят по понятным причинам, только optimistic concurrency.
Re[16]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 29.09.17 05:17
Оценка:
Здравствуйте, Sinix, Вы писали:

S>Здравствуйте, Poul_Ko, Вы писали:


P_K>>Или не то имелось в виду?

S>Это, если все вычитанные данные сохранятся с самим заказом. Если нет — берутся id записей истории и подставляются в заказ. Т.е. заказ сохраняется строго в том состоянии, в каком его подтвердил пользователь.
А зачем? Я в том плане что это не требуется бизнесу. Бизнес должен видеть — заказ подтверждён, заказ по договору Х, значит условия договора и заказ не противоречат друг другу. Точка. Никаких версий и историй записей им не нужно.

P_K>>Условия меняются после подтверждения — если они противоречат уже подтверждённым заказам, то ПО отвергает изменения, юзеры должны сначала устранить проблему, например, отменить конфликтующие заказы. Но они должны это сделать явно.

S>Вот это не сработает. Нельзя "отменить конфликтующий заказ" простым велением левой ноги разработчика. Или такой биз-процесс обоснован реальными нуждами бизнеса и описан в ТЗ, или тот, кто занимался анализом требований схалтурил
Причём здесь разработчик? Заказ отменяется пользователем. Пишу же, пользователи должны это сделать явно!

S>>>В тех редких ситуациях, когда это не так, производится доппроверка перед заключением договора, которая уменьшает окно рассогласования до 2..3 минут.

P_K>>Что такое "окно рассогласования"?
S>Грубо говоря — ожидаемое время между "получили данные" и "сохранили обратно". Т.е. проверять, при неудаче сохранения (что-то поменялось) показать обновлённые данные пользователю и так по новой.
А как отловить вот это неудачу сохранения ("что-то поменялось") в многопоточной среде без блокировок?

P_K>>При коррекции условий договора происходит проверка всех подтверждённых заказов по данному договору, если есть заказы, которые идут в разрез с новыми условиями, то коррекции не происходит, список показывается пользователю и бизнес-операция завершается. Пользователь должен исправить заказы и попытаться ещё раз изменить условия договора.

S>Ну, раньше про "показывается пользователю" речи не шло, речь шла про блокировки.
В чём проблема с показыванием? Меняем договор, для проверки ищем все заказы по нему. Они у нас есть, уже загружены, нашли конфликтующие — всё, абзац, изменения не принимаются, бизнес-операция завершается, её результат — "не удалось + список заказов". Показывание и действия пользователя по исправлению идут своим чередом, как обычные операции, вне бизнес-операции изменения договора. Об этом было написано же.
Brainbench transcript #6370594
Re[17]: Блокировки в бизнес-слое
От: Sinix  
Дата: 29.09.17 05:30
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

S>>Это, если все вычитанные данные сохранятся с самим заказом. Если нет — берутся id записей истории и подставляются в заказ. Т.е. заказ сохраняется строго в том состоянии, в каком его подтвердил пользователь.

P_K>А зачем? Я в том плане что это не требуется бизнесу. Бизнес должен видеть — заказ подтверждён, заказ по договору Х, значит условия договора и заказ не противоречат друг другу. Точка. Никаких версий и историй записей им не нужно.
Требуется. К примеру, для розничной торговли см п. 1 ст. 500 ГК РФ

1. Покупатель обязан оплатить товар по цене, объявленной продавцом в момент заключения договора розничной купли-продажи, если иное не предусмотрено законом, иными правовыми актами или не вытекает из существа обязательства.

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


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

P_K>Причём здесь разработчик? Заказ отменяется пользователем. Пишу же, пользователи должны это сделать явно!
Опс и прошу прощения. Неверно понял


S>>Грубо говоря — ожидаемое время между "получили данные" и "сохранили обратно". Т.е. проверять, при неудаче сохранения (что-то поменялось) показать обновлённые данные пользователю и так по новой.

P_K>А как отловить вот это неудачу сохранения ("что-то поменялось") в многопоточной среде без блокировок?
Через optimistic concurrency — update проходит только если не поменялось состояние заказа (например, чексумма).


S>>Ну, раньше про "показывается пользователю" речи не шло, речь шла про блокировки.

P_K>В чём проблема с показыванием?
Нельзя удерживать блокировку на время показа. Пользователь затормозил на 5 минут — все прочие связанные операции встали на те же 5 минут.
Re[18]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 29.09.17 06:25
Оценка:
Здравствуйте, Sinix, Вы писали:
S>Требуется. К примеру, для розничной торговли см п. 1 ст. 500 ГК РФ
Не применимо. Здесь ситуация другая, требования я описал, давайте своих добавлять не будем

S>>>Грубо говоря — ожидаемое время между "получили данные" и "сохранили обратно". Т.е. проверять, при неудаче сохранения (что-то поменялось) показать обновлённые данные пользователю и так по новой.

P_K>>А как отловить вот это неудачу сохранения ("что-то поменялось") в многопоточной среде без блокировок?
S>Через optimistic concurrency — update проходит только если не поменялось состояние заказа (например, чексумма).
А состояние заказа и не поменяется. Поменяется состояние связанного договора.
Или предлагаете при изменении договора менять версии всех связанных с ним заказов, даже неподтверждённых? Как-то странно это будет...

S>>>Ну, раньше про "показывается пользователю" речи не шло, речь шла про блокировки.

P_K>>В чём проблема с показыванием?
S>Нельзя удерживать блокировку на время показа. Пользователь затормозил на 5 минут — все прочие связанные операции встали на те же 5 минут.
Блокировки не будет. Список заказов — это результат операции, а не её часть. На время показывания ничего не блокируется, да и времени этого нет. Это просто окно "Упс! Не можем изменить даты действия договора — по нему есть подтверждённые заказы, попадающие в интервал когда договор перестаёт действовать. Это заказы №№1, 2 и 5.", и одна кнопка "Закрыть".
Brainbench transcript #6370594
Re[19]: Блокировки в бизнес-слое
От: · Великобритания  
Дата: 29.09.17 07:06
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

P_K>// Почему здесь Agreement? Подтверждаем-то мы заказ!

Эээ.. Да я запутался в твоей модели данных. Но какая разница-то? Суть та же. Версию менять и проверять надо у того, что зависит от изменения данных:
BEGIN TRANS;
UPDATE Agreement SET startDate = ... WHERE id=1;
UPDATE Order SET version=version+1 WHERE agreementId=1;// заметь, тут несколько ордеров может быть изменено.
COMMIT;

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

SELECT version, ... FROM Order...;
var currentVersion = version;
//check
//check
SELECT startDate, ... FROM Agreement...;
//check
//...sloooow check....
//check
UPDATE Order SET confirmed='YES' WHERE version=currentVersion...

А этот процесс не обязательно должен быть в одной транзакции — каждая sql-команда может выполняться сама по себе — транзакции будут короткоживущими, что есть хорошо.

P_K>·>Ну и модифицирующие операции (все или как минимум те, которые могут влиять на проверки) должны изменять версию:

P_K>Это классика оптимистической блокировки, но работает она в пределах одного агрегата, а у нас здесь два (в реальности конечно больше)...
Так оно обобщается до произвольного числа агрегатов.
Можно даже версировать несколько сущностей, а потом атомарной операцией (т.е. обернутой в транзакцию) делать проверку совпадения всех версий и SET confirmed='YES' если всё совпало.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Отредактировано 29.09.2017 7:11 · . Предыдущая версия .
Re[19]: Блокировки в бизнес-слое
От: Sinix  
Дата: 29.09.17 07:33
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

S>>Требуется. К примеру, для розничной торговли см п. 1 ст. 500 ГК РФ

P_K>Не применимо. Здесь ситуация другая, требования я описал, давайте своих добавлять не будем
Если бизнес работает с покупками, то рано или поздно подобное требование точно появится. Аналогичные правила торговли есть практически во всех странах.


P_K>Или предлагаете при изменении договора менять версии всех связанных с ним заказов, даже неподтверждённых? Как-то странно это будет...

Угу. Или любой другой способ, который позволит определить конфликт при сохранении. Иначе уведомить клиента об изменениях условия договора будет проблематично.
Re[20]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 29.09.17 08:12
Оценка:
Здравствуйте, ·, Вы писали:
.>Версию менять и проверять надо у того, что зависит от изменения данных
Теперь идею понял.
В реальности изменения будут каскадными — меняем договор -> меняются версии связанных заказов -> меняются версии других связанных сущностей... Где-то в этой цепочке могут появляться кольца.
Интересно подумать как будет решаться ситуация когда меняется сама связь, например, для заказа выбирается другой договор и нужно проверить что он подходит.
Или когда условие имеет вид "чего-то не существует" (мы же не можем менять версию того, чего нет). Наверно придётся вводить какие-то дополнительные сущности или флаги.
В общем, спасибо, отправная точка есть, нужно будет это всё хорошо обмозговать с товарищами.
Brainbench transcript #6370594
Re[3]: Блокировки в бизнес-слое
От: wildwind Россия  
Дата: 29.09.17 08:24
Оценка:
Здравствуйте, IB, Вы писали:

IB>Для всего этого не нужно писать свой менеджер блокировок. В нормальных базах обычно предоставляется механизм позволяющий получить доступ к менеджеру блокировок базы и воспользоваться его услугами для блокировки прикладных объектов. Гораздо правильнее воспользоваться им, если уж возникнет такая нужда, чем городить собственный менеджер.


Да, если приложение жестко привязано к одной СУБД. Нет, если необходима поддержка нескольких СУБД, в том числе и тех, которые такого механизма не предоставляют. В случае 1С это так.

W>>Но за это приходится платить дорогую цену. Все изменения бизнес объектов должны идти через этот менеждер блокировок. Например, мы не можем выполнить сложный многострочный UPDATE или DELETE, так как наш менеджер блокировок не может проверить, не относятся ли затронутые им строки к заблокированным объектам. Из-за этого сильно страдает производительность на запись.

IB>Отдельный плюс менеджера блокировок который встроен в БД состоит в том, что о нем знают транзакции БД.

Да, но в вышеописанном сценарии это не помогает.
Re[3]: Блокировки в бизнес-слое
От: itslave СССР  
Дата: 29.09.17 08:52
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

P_K>Спасибо, интересно.

P_K>Если я правильно понял, то суть решения в исключении источника проблемы — многопоточного исполнения. Все операции выполняются последовательно, вокруг этого накручивается некая инфраструктура, повышающая производительность.
P_K>Боюсь что для проекта в его текущем состоянии переход на такую модель потребует слишком много сил и времени.
Можно поэтапно перейти. Начать с того чтобы просто ввести очередь событий, затем в нее писать, не меняя функциональности, затем научиться проигрывать события и поэтапно переводить проект на новые рельсы.
Re[19]: Блокировки в бизнес-слое
От: XuMuK Россия  
Дата: 29.09.17 08:58
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

P_K>Применение — это коммит?

да

P_K>"Подтверждения договора" — нет такого в бизнес-процессе, имелось в виду подтверждение заказа?

пардон, везде вместо договора имелся в виду заказ.

P_K>
P_K>if (GetActualContractVersion(contract.Id) == contract.Version) then CommitTransaction() else RollbackTransaction();
P_K>

P_K>По идее вся последняя строчка должна отработать атомарно прямо на уровне базы, тогда решение будет рабочее, согласен. Но как её такую сделать?
да, ровно это я и имел в виду (как уже объяснили ниже). с версиями заказа не нужно блокировать всю базу для проверки его статуса, достаточно чтобы с момента начала проверки версия заказа не изменилась, тогда изменение статуса заказа на "подтвержден" можно сделать с блокировкой таблицы заказов (или даже конкретного заказа в таблице) в хранимке или в коде. профит в том, что эта операция короткая и не затрагивает/не блокирует другие таблицы.
Re[21]: Блокировки в бизнес-слое
От: · Великобритания  
Дата: 29.09.17 09:10
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

.>>Версию менять и проверять надо у того, что зависит от изменения данных

P_K>Теперь идею понял.
P_K>В реальности изменения будут каскадными — меняем договор -> меняются версии связанных заказов -> меняются версии других связанных сущностей... Где-то в этой цепочке могут появляться кольца.
Кольцо можно разомкнуть введя дополнительную версируемую сущность, объединяющую элементы кольца.

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

Да то же самое:
UPDATE Order SET agreementId=2, Version=Version+1 WHERE id=12345;


P_K>Или когда условие имеет вид "чего-то не существует" (мы же не можем менять версию того, чего нет). Наверно придётся вводить какие-то дополнительные сущности или флаги.

Да всё то же:
INSERT INTO AgreementDetail(agreementId, etc...) VALUES(1, data...);// или DELETE FROM...
UPDATE Order SET Version=Version+1 WHERE agreementId=1;

Т.е. если AgreementDetail не существует — ты это увидишь что его нет в процессе проверок. Если он добавился (или удалился) конкурентно — это изменит версию ордера и вызовет откат подтверждения.

Одно плохо — надо не забывать изменять версию договора при изменении чего либо что хоть как-то может повлият на его подтверждение — это никак не проверяется явно. А если забыл — будут труднообнаружимые баги. Иначе говоря, доказать корректность системы — очень тяжело. Например, кто-то добавил код, который модифицирует поле AgreementDetail, но забыл пнуть версию соответсвующих договоров — бага — и фиг найдёшь.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[13]: Блокировки в бизнес-слое
От: IB Австрия http://rsdn.ru
Дата: 29.09.17 10:36
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

P_K>Допустим всё приемлемо работает на таких блокировках. Тут появляется новая фича, в перечень проверок добавляется чтение новой сущности (а значит и блокировка). Потом добавляется ещё одна фича, потом ещё одна. И это не в одно бизнес-действие, а в несколько. В итоге проверки блокируют условно пол-базы и "привет, дедлоки!"

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

P_K>Выходит из-под контроля — потому что писателям бизнес-кода неизвестно чего там заблокирует база. Это отдельный вопрос, для которого наверно нужен грамотный DBA.

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

P_K>Вот тут позволю себе не согласиться. Пусть наш бизнес-код в serializable-транзакции вычитывает какую-то сущность из базы (несколько строк из пар таблиц). Что заблокирует база? Только прочитанные строки? Какие-то свои страницы (т.е. заранее непредсказуемый набор сущностей)? Всю таблицу (все сущности)? Вот я не могу ответить на этот вопрос, это надо очень глубоко закапываться в базу.

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

P_K>Если блокировки не автоматические, предсказуемые, то единственным аргументом против базы я вижу вопрос производительности. Зачем бегать в базу, если можно разрулить в памяти? Масштабируемость (несколько app-серверов) на требуется.

Если не требуется масштабируемости, то вопрос производительности не должен волновать.
Простое решение в лоб — гоните все ваши зменения через один глобальный мьютекс, должно быть норм, дешево и сердито. Писать же свой менеджер блокировок, особенно не имея преставления как это работает в базах... Да проще разобраться как БД работает.
Мы уже победили, просто это еще не так заметно...
Re[4]: Блокировки в бизнес-слое
От: IB Австрия http://rsdn.ru
Дата: 29.09.17 10:45
Оценка:
Здравствуйте, wildwind, Вы писали:

W>Да, если приложение жестко привязано к одной СУБД.

Не жестко к одной СУБД, а нет требования работать одновременно с несколькими СУБД. А приложений без таких требований 99.9%

W> Нет, если необходима поддержка нескольких СУБД, в том числе и тех, которые такого механизма не предоставляют. В случае 1С это так.

Именно это я и имел ввиду, когда говорил, что у 1С это не от хорошей жизни. Но написание собственного менеджера блокировок задача совсем не тривиальная. Топикстартер не 1С же пишет.

W>Да, но в вышеописанном сценарии это не помогает.

Должен помогать, поскольку используется все тот же менеджер блокировок, то он как раз знает, какие строки относятся к заблокированным объектам. Иными словами, ваше требование про изменение бизнес-объектов через менеджер блокировок выполняется автоматически.
Мы уже победили, просто это еще не так заметно...
Re[14]: Блокировки в бизнес-слое
От: Qulac Россия  
Дата: 29.09.17 10:52
Оценка:
Здравствуйте, IB, Вы писали:

IB>Здравствуйте, Poul_Ko, Вы писали:


IB>Писать же свой менеджер блокировок, особенно не имея преставления как это работает в базах... Да проще IB>разобраться как БД работает.


В принципе он не так сложен, почти для всего хватит такой модели:

  //Получение блокировки 
  public void Lock(Object data,Write/Read mode,Object owner)
  
  //снятие всех блокировок данного владельца
  public void UnLock(Object owner);


Ответственность за исполнение блокировок лежит на клинском коде, т.е. всегда прежде чем трогать объект, получаем на него блокировку. А вот менеджер транзакций это уже более сложная задача.
Программа – это мысли спрессованные в код
Отредактировано 29.09.2017 11:11 Qulac . Предыдущая версия . Еще …
Отредактировано 29.09.2017 11:11 Qulac . Предыдущая версия .
Re[2]: Блокировки в бизнес-слое
От: · Великобритания  
Дата: 29.09.17 12:49
Оценка:
Здравствуйте, Sinclair, Вы писали:

P_K>>Какие другие решения можете посоветовать?

S>Философски — никаких других решений нет. Вы приводите пример конкурирующих изменений. Все способы решения этой проблемы давно известны.
S>Отличаются только детали реализации.
S>К примеру, "дедлок", которого вы так боитесь — это, фактически, обнаружение конфликта изменений.
Ты что-то путаешь. Обычно deadlock это взаимоблокировка в результате ошибки имплементации. Скажем, классический пример — перевод денег с аккаунта A1 на аккаунт A2 при наивной имплементации возможен deadlock если аккаунты будут будут блокироваться в произвольном порядке — один пытается заблокировать A1, затем A2, а второй пытается A2, затем A1 — то они зависнут. Если же ввести отношение порядка и блокировать всегда A1,A2 — то дедлоков никаких не будет.

S>Его проявление в мире пессимистичных блокировок.

S>Какой бы способ вы ни выбрали, у вас всё равно возможна ситуация "ой, между тем, как мы показали вам цену, и вашим нажатием на кнопку Save, произошли изменения. Будете повторять?".
Так это OL. А в PL будет просто "Не могу открыть Соглашение, т.к. кто-то сейчас проверяет Заказ связанный с этим Соглашением. Попробуйте позже." или просто песочные часики.

S>В pessimistic locking это будет как раз deadlock. В Optimistic locking это будет "расхождение таймстампов".

С OL может возникнуть livelock и starvation... что тоже неприятно.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[10]: Блокировки в бизнес-слое
От: itslave СССР  
Дата: 29.09.17 13:03
Оценка:
Здравствуйте, IB, Вы писали:

IB>Для этой проблемы другого решения нет.

Есть, ибо транзакции- это один из инструментов монопольного доступа к жанным. Выше уже упоминали LMAX — еще один инструмент.
Re[13]: Блокировки в бизнес-слое
От: itslave СССР  
Дата: 29.09.17 13:07
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

P_K>Либо перенести блокировки с базы на уровень выше — в бизнес-слой. Вот с целью ресёча такого решения я и затеял данную тему.


Вот это тот самый случай, когда лечение хуже болезни. Нормально реализовать транзакционность в бизнес слое — весьма нетривиальная задачка, смело умножайте прикидочные эстимейты на 10 и прибавьте 100% вероятность иметь невоспроизводимые баги месяцами.
Re[14]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 29.09.17 13:59
Оценка:
Здравствуйте, itslave, Вы писали:
P_K>>Либо перенести блокировки с базы на уровень выше — в бизнес-слой. Вот с целью ресёча такого решения я и затеял данную тему.
I>Вот это тот самый случай, когда лечение хуже болезни. Нормально реализовать транзакционность в бизнес слое — весьма нетривиальная задачка, смело умножайте прикидочные эстимейты на 10 и прибавьте 100% вероятность иметь невоспроизводимые баги месяцами.

Да тут у нас всё просто — любой не-read метод app-слоя всегда оборачивается в транзакцию, и либо целиком выполняется, либо нет. Внутри каких-то частичных откатов или коммитов не допускаем.
Другими словами, бизенс-слой вообще не знает о транзакциях и не рулит ими, они снаружи. По соглашению любое исключение приводит к откату транзакции.
Brainbench transcript #6370594
Re[15]: Блокировки в бизнес-слое
От: itslave СССР  
Дата: 29.09.17 14:04
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

P_K>Да тут у нас всё просто — любой не-read метод app-слоя всегда оборачивается в транзакцию, и либо целиком выполняется, либо нет. Внутри каких-то частичных откатов или коммитов не допускаем.

P_K>Другими словами, бизенс-слой вообще не знает о транзакциях и не рулит ими, они снаружи. По соглашению любое исключение приводит к откату транзакции.

На каждый чих блокировать абсолютно все? Упретесь в перфоманс моментально.
Re[16]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 29.09.17 14:53
Оценка:
Здравствуйте, itslave, Вы писали:
P_K>>Да тут у нас всё просто — любой не-read метод app-слоя всегда оборачивается в транзакцию, и либо целиком выполняется, либо нет. Внутри каких-то частичных откатов или коммитов не допускаем.
P_K>>Другими словами, бизенс-слой вообще не знает о транзакциях и не рулит ими, они снаружи. По соглашению любое исключение приводит к откату транзакции.
I>На каждый чих блокировать абсолютно все? Упретесь в перфоманс моментально.
Это делается для обеспечения атомарности. В идеале видим использование транзакций только для атомарности и изоляции (READ COMMITTED), а блокировки через что-то другое, например, свой механизм в бизнес-слое.
Brainbench transcript #6370594
Re[5]: Блокировки в бизнес-слое
От: wildwind Россия  
Дата: 01.10.17 05:59
Оценка:
Здравствуйте, IB, Вы писали:

W>>Да, но в вышеописанном сценарии это не помогает.

IB>Должен помогать, поскольку используется все тот же менеджер блокировок, то он как раз знает, какие строки относятся к заблокированным объектам. Иными словами, ваше требование про изменение бизнес-объектов через менеджер блокировок выполняется автоматически.

Можно подробнее? Насколько мне известно, блокировки, полученные sp_getapplock, никак не связаны с какими-то объектами в БД. Из семантика полностью определяется кодом приложения. Но может, я чего-то не знаю...
Re[6]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 01.10.17 13:09
Оценка:
Здравствуйте, wildwind, Вы писали:
W>Можно подробнее? Насколько мне известно, блокировки, полученные sp_getapplock, никак не связаны с какими-то объектами в БД. Из семантика полностью определяется кодом приложения. Но может, я чего-то не знаю...
Сам не пользовался, но из документации понял что блокировка всегда к чему-то привязывается: либо к транзакции, либо к сессии.
Brainbench transcript #6370594
Re[7]: Блокировки в бизнес-слое
От: wildwind Россия  
Дата: 01.10.17 16:11
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

P_K>Сам не пользовался, но из документации понял что блокировка всегда к чему-то привязывается: либо к транзакции, либо к сессии.


Речь не об этом. Почитай всю нашу с IB ветку дискуссии.
Re[9]: Блокировки в бизнес-слое
От: Sinclair Россия https://github.com/evilguest/
Дата: 02.10.17 04:05
Оценка:
Здравствуйте, Poul_Ko, Вы писали:
P_K>Теперь поменяем уровень изоляции на SERIALIZABLE. Всё заработает как надо — необходимые блокировки будет делать база. Чем это плохо — см. первое сообщение.
Достаточно repeatable read.

P_K>Конечно в правой части на самом деле будет не такой апдейт — там будет сначала SELECT (поиск всех неподтверждённых заказов), а потом серия апдейтов — по одному на каждый заказ. Но принципиально это ничего не меняет, даже наоборот, повышает вероятность такой логической неконсистентности.

Не надо так делать.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[10]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 02.10.17 07:33
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>Здравствуйте, Poul_Ko, Вы писали:

P_K>>Теперь поменяем уровень изоляции на SERIALIZABLE. Всё заработает как надо — необходимые блокировки будет делать база. Чем это плохо — см. первое сообщение.
S>Достаточно repeatable read.
В этом конкретном случае может быть да. Но в целом когда есть проверка типа "чего-то нет" (EXISTS, COUNT) и нужно обеспечить чтобы в ходе транзакции она оставалась неизменной, то приходится использовать serializable.
Да, мы выяснили, что наверняка такие проверки можно свести к проверкам значений свойств и использовать repeatable read, но это требует дополнительных телодвижений, вплоть до введения новых принципиально бесполезных для бизнеса сущностей.

P_K>>Конечно в правой части на самом деле будет не такой апдейт — там будет сначала SELECT (поиск всех неподтверждённых заказов), а потом серия апдейтов — по одному на каждый заказ. Но принципиально это ничего не меняет, даже наоборот, повышает вероятность такой логической неконсистентности.

S>Не надо так делать.
А другого пути я не вижу. Это в примере расчёт простой — умножить одно на другое, по факту же там играет бизнес-логика. А она в коде и оперирует загруженными сущностями. То есть, пересчёт быдет выглядеть как-то так:
// Найти заказы для пересчёта
foreach(var order = FindOrdersToRecalc(contractId))
{
    // Пересчитать каждый заказ
    _orderingService.RecalculateOrder(order);
}
Brainbench transcript #6370594
Re[12]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 02.10.17 10:03
Оценка:
Здравствуйте, Sinclair, Вы писали:
S>Охх. Лень искать исчерпывающий пост. Кратко, тезисно:
Не хочу обидеть, но это всё давно мне известно. Согласен, во многом здесь вопрос аккуратности и понимания.

P_K>>Да, мы выяснили, что наверняка такие проверки можно свести к проверкам значений свойств и использовать repeatable read, но это требует дополнительных телодвижений, вплоть до введения новых принципиально бесполезных для бизнеса сущностей.

S>Вообще все сущности для бизнеса принципиально бесполезны. Вопрос в том, как решить задачу техническими средствами, а не как сделать так, чтобы структура таблиц была похожа на графы бланка типового договора.
Под бизнесом я имел в виду domain model. Это та модель бизнеса, которая реализована в ПО (думаю определение вам итак известно).
Получается что для уменьшения используемого уровня изоляции в определённых условиях необходимо вводить новые атрибуты и/или сущности, нужно написать код в бизнес-слое, который бы оперировал ими. Это получаются своеобразные "индексы", необходимость которых получается обусловленной чисто техническими нуждами. Вот я о чём.

S>Во-первых надо понять, как вообще это всё выглядит с точки зрения СУБД.

В описанном мной коде и используемом подходе на СУБД это ляжет как-то так:
begin serializable transaction
update products set price = 10600 where id = 101;    // обновить прайс

select orders where state = 'N' and productId = 101;    // найти заказы

// среднее звено посчитало и сохраняет найденные ранее заказы
update order set cost = 23232 where id = 1111;
update order set cost = 65465 where id = 2222;
...

commit transaction


S>Способов у этого несколько:

S>1. Воспользоваться хранимыми процедурами.
Внутри хранимки будет исполняться по сути тот же SQL, что и в случае когда логика отдельно. Только это будет работать быстрее, в рамках одного соединения и т.д. Но проблемы конкурренции по-моему остаются ровно те же самые. Точно также, если внутри хранимки два раза прочитать одну и ту же строку при уровне изоляции READ COMMITTED, то можем поиметь разные значения и нарушение всей логики.

S>2. Оставить логику в миддл-тир, но оборудовать все чтения данных, которые будут использованы в рамках бизнес-транзакции, рукопашными блокировками (например, через sp_getapplock в случае MS SQL).

Во что это выльется на СУБД? Если не ошибаюсь, то во что-то вроде такого:
begin read committed transaction

sp_getapplock('Products', 'Exclusive');
sp_getapplock('Orders', 'Exclusive');

update products set price = 10600 where id = 101;    // обновить прайс

select orders where state = 'N' and productId = 101;    // найти заказы

// среднее звено посчитало и сохраняет найденные ранее заказы
update order set cost = 23232 where id = 1111;
update order set cost = 65465 where id = 2222;
...

sp_releaseapplock('Orders');
sp_releaseapplock('Products');

commit transaction

S>3. Оставить логику в миддл-тир, но переписать её на продвинутые средства вроде linq[2db]. Т.е. получить функциональный аналог п.1, но с нормальной декомпозицией и современным языком, а не процедурным языком образца 1993 года.
Подскажите пожалуйста как это решение будет выглядеть с точки зрения СУБД. Или ссылку на похожий пример дайте.
Если там внутри будет что-то вроде update order set cost = price * quantity (т.е. логика расчёта), то боюсь что такое возможно далеко не всегда. Не любую логику можно выразить через SQL-запрос.
Brainbench transcript #6370594
Re[4]: Блокировки в бизнес-слое
От: Sharov Россия  
Дата: 02.10.17 12:33
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>Что произойдёт в пессимистике? Одна из транзакций будет отстрелена с ошибкой, вторая — успешно завершится.


Разве отстрелена? Или она(транзакция) просто встанет в очередь? Или же поведение и в опт. и пес. блокировке зависит от модели изоляции.
Потому как пес. модель мне казалось эквивалентна сериализации... У меня небольшая путаница с моделью блокировок и уровнем изоляции -- зачем надо
париться над уровнем изоляции в случае пессимистичной блокировки?
Кодом людям нужно помогать!
Re[13]: Блокировки в бизнес-слое
От: Sinclair Россия https://github.com/evilguest/
Дата: 03.10.17 08:08
Оценка:
Здравствуйте, Poul_Ko, Вы писали:


S>>Во-первых надо понять, как вообще это всё выглядит с точки зрения СУБД.

P_K>В описанном мной коде и используемом подходе на СУБД это ляжет как-то так:
P_K>
P_K>begin serializable transaction
P_K>update products set price = 10600 where id = 101;    -- обновить прайс

P_K>select orders where state = 'N' and productId = 101;    -- найти заказы

P_K>// среднее звено посчитало и сохраняет найденные ранее заказы
Вот этот момент - опасен. Надо профилировать среднее звено, чтобы "посчитать заказы" не привело к затягиванию транзакции. Ещё опаснее то, что среднее звено может подтянуть ещё какие-то данные, и не факт, что в той же транзакции. 
P_K>update order set cost = 23232 where id = 1111;
P_K>update order set cost = 65465 where id = 2222;
P_K>...

P_K>commit transaction
P_K>


S>>Способов у этого несколько:

S>>1. Воспользоваться хранимыми процедурами.
P_K>Внутри хранимки будет исполняться по сути тот же SQL, что и в случае когда логика отдельно. Только это будет работать быстрее, в рамках одного соединения и т.д. Но проблемы конкурренции по-моему остаются ровно те же самые. Точно также, если внутри хранимки два раза прочитать одну и ту же строку при уровне изоляции READ COMMITTED, то можем поиметь разные значения и нарушение всей логики.
Конечно. Придётся делать repeatable read.
S>>2. Оставить логику в миддл-тир, но оборудовать все чтения данных, которые будут использованы в рамках бизнес-транзакции, рукопашными блокировками (например, через sp_getapplock в случае MS SQL).
P_K>Во что это выльется на СУБД? Если не ошибаюсь, то во что-то вроде такого:
P_K>
P_K>begin read committed transaction

P_K>sp_getapplock('Products', 'Exclusive');
P_K>sp_getapplock('Orders', 'Exclusive');

P_K>update products set price = 10600 where id = 101;    // обновить прайс

P_K>select orders where state = 'N' and productId = 101;    // найти заказы

P_K>// среднее звено посчитало и сохраняет найденные ранее заказы
P_K>update order set cost = 23232 where id = 1111;
P_K>update order set cost = 65465 where id = 2222;
P_K>...
P_K>sp_releaseapplock('Orders');
P_K>sp_releaseapplock('Products');

P_K>commit transaction
P_K>


Нет.
-- процедура смены цены
begin transaction -- read_committed

  sp_getapplock('Product_101', 'Exclusive'); -- захватили лок _нужного нам_ продукта. 
  -- Если в это время висит незакрытая транзакция по добавлению заказа, то мы встанем в ожидании.

  update products set price = 10600 where id = 101;    // обновить прайс

  select orders where state = 'N' and productId = 101;    // найти заказы

  -- среднее звено посчитало и сохраняет найденные ранее заказы
  update order set cost = 23232 where id = 1111;
  update order set cost = 65465 where id = 2222;
  sp_releaseapplock('Product_101'); -- не обязательно - коммит/роллбэк транзакции сам отпустит лок
commit transaction

-- процедура добавления заказа
begin transaction -- read_committed
  sp_getapplock('Product_101', 'Exclusive'); -- захватили лок _нужного нам_ продукта. 
  -- Если в это время висит незакрытая транзакция по изменению цены на продукт, то мы встанем в ожидании.
  insert into orders(cost, productId, quantity, state) 
              values(23232, 101, 50, 'N')
  sp_releaseapplock('Product_101'); -- не обязательно - коммит/роллбэк транзакции сам отпустит лок
commit transaction

S>>3. Оставить логику в миддл-тир, но переписать её на продвинутые средства вроде linq[2db]. Т.е. получить функциональный аналог п.1, но с нормальной декомпозицией и современным языком, а не процедурным языком образца 1993 года.
P_K>Подскажите пожалуйста как это решение будет выглядеть с точки зрения СУБД. Или ссылку на похожий пример дайте.
P_K>Если там внутри будет что-то вроде update order set cost = price * quantity (т.е. логика расчёта), то боюсь что такое возможно далеко не всегда. Не любую логику можно выразить через SQL-запрос.
Да, именно что-то в этом роде и будет. Я вас уверяю, в бизнес-логике ничего сложнее ветвлений и арифметики не встречается. Никаких тебе логарифмов и функций Бесселя там нет.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[14]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 03.10.17 08:47
Оценка:
Здравствуйте, Sinclair, Вы писали:
S>
S>-- процедура смены цены
S>begin transaction -- read_committed

S>  sp_getapplock('Product_101', 'Exclusive'); -- захватили лок _нужного нам_ продукта. 
S>  -- Если в это время висит незакрытая транзакция по добавлению заказа, то мы встанем в ожидании.

S>  update products set price = 10600 where id = 101;    // обновить прайс

S>  select orders where state = 'N' and productId = 101;    // найти заказы

S>  -- среднее звено посчитало и сохраняет найденные ранее заказы
S>  update order set cost = 23232 where id = 1111;
S>  update order set cost = 65465 where id = 2222;
S>  sp_releaseapplock('Product_101'); -- не обязательно - коммит/роллбэк транзакции сам отпустит лок
S>commit transaction

S>-- процедура добавления заказа
S>begin transaction -- read_committed
S>  sp_getapplock('Product_101', 'Exclusive'); -- захватили лок _нужного нам_ продукта. 
S>  -- Если в это время висит незакрытая транзакция по изменению цены на продукт, то мы встанем в ожидании.
S>  insert into orders(cost, productId, quantity, state) 
S>              values(23232, 101, 50, 'N')
S>  sp_releaseapplock('Product_101'); -- не обязательно - коммит/роллбэк транзакции сам отпустит лок
S>commit transaction
S>

Раз мы прочитали заказы и собираемся их обновлять, то по-хорошему их также нужно блокировать. Чтобы никто другой не мог параллельно изменить / удалить заказ.
Хотя в этом случае может помочь как раз таки оптимистическая блокировка. Тогда апдейты должны в коде выше должны снабжаться чем-то вроде 'and version = 234'.

S>Я вас уверяю, в бизнес-логике ничего сложнее ветвлений и арифметики не встречается. Никаких тебе логарифмов и функций Бесселя там нет.

Речь не об этом. При определённой сложности (в плане количества ветвлений) простая реализация логики в виде одного метода становится невозможной. Получаем ад из ветвлений и всяких флагов... В итоге всё это рефакторится в отдельные классы (например, что-то вроде стратегий), приобретает понятную форму и тестируемость. На хранимых процедурах такой финт невозможен, думаю согласитесь. Свести всё к запросам? Интересная идея, хорошо бы увидеть реальные примеры такого дизайна чтобы понять границы применимости.
Brainbench transcript #6370594
Re[15]: Блокировки в бизнес-слое
От: Sinclair Россия https://github.com/evilguest/
Дата: 03.10.17 10:41
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

P_K>Раз мы прочитали заказы и собираемся их обновлять, то по-хорошему их также нужно блокировать. Чтобы никто другой не мог параллельно изменить / удалить заказ.

В приведённой архитектуре при любом изменении заказа нужно получать блокировки на продукт — ровно по той же причине.
-- внесение изменения в заказ
begin tran -- read_committed
select @old_productId = productId from orders with updlock where id = @order_id -- заранее захватываем U-блокировку, чтобы избежать дедлоков 
set @product1 = min(@old_productId, @new_productId) -- предотвращаем дедлоки, вводя строгую упорядоченность в захвате локов на продукт
set @product2 = max(@old_productId, @new_productId)
sp_getapplock('product_'+@product1)
sp_getapplock('product_'+@product2)

update orders set productId = @new_productId, cost = @new_Cost, ..., where id = @order_id

commit tran

-- удаление заказа
begin tran -- read_committed
select @productId = productId from orders with updlock where id = @order_id -- заранее захватываем U-блокировку, чтобы избежать дедлоков 
sp_getapplock('product_'+@productIв)
delete from orders set productId = @new_productId, cost = @new_Cost, ... where id = @order_id

commit tran


P_K>Речь не об этом. При определённой сложности (в плане количества ветвлений) простая реализация логики в виде одного метода становится невозможной. Получаем ад из ветвлений и всяких флагов... В итоге всё это рефакторится в отдельные классы (например, что-то вроде стратегий), приобретает понятную форму и тестируемость. На хранимых процедурах такой финт невозможен, думаю согласитесь.

Поэтому я вам и говорю про Linq. Он позволяет писать аккуратный и понятный код, который будет транслироваться в корректный SQL без лишних усилий с вашей стороны.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Отредактировано 03.10.2017 10:42 Sinclair . Предыдущая версия .
Re[16]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 03.10.17 11:18
Оценка:
Здравствуйте, Sinclair, Вы писали:
S>Поэтому я вам и говорю про Linq. Он позволяет писать аккуратный и понятный код, который будет транслироваться в корректный SQL без лишних усилий с вашей стороны.
Эту мысль я понял.
Смотрите, ведь linq будет оперировать сущностями слоя доступа к данным, а не сущностями бизнес-уровня. Получаем переезд части логики в DAL.
Конечно, если бизнес-сущности и таблицы соотносятся очень просто, то можно написать linq на бизнес-сущностях и это всегда транслируется в SQL. Но к сожалению так бывает не всегда.
Поэтому пока складывается мнение что подход интересный, но работает в относительно простых случаях.
Brainbench transcript #6370594
Re[17]: Блокировки в бизнес-слое
От: Sinclair Россия https://github.com/evilguest/
Дата: 03.10.17 16:45
Оценка:
Здравствуйте, Poul_Ko, Вы писали:
P_K>Смотрите, ведь linq будет оперировать сущностями слоя доступа к данным, а не сущностями бизнес-уровня. Получаем переезд части логики в DAL.
P_K>Конечно, если бизнес-сущности и таблицы соотносятся очень просто, то можно написать linq на бизнес-сущностях и это всегда транслируется в SQL. Но к сожалению так бывает не всегда.
P_K>Поэтому пока складывается мнение что подход интересный, но работает в относительно простых случаях.
Хотелось бы убедительный пример.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[18]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 04.10.17 07:57
Оценка:
Здравствуйте, Sinclair, Вы писали:
S>Хотелось бы убедительный пример.
100%-но убедительный пример привести не смогу, так как конкретно это технологией не пользовался и реального опыта её применения не имею.
Но могу описать на основании каких представлений у меня сложилось озвученное мнение.
Во-первых, то что бизнес-сущности не всегда один-к-одному соответствуют таблицам я думаю вы согласитесь. В простейших случаях это следствие преднамеренной денормализации (храним список Id строкой, не охота городить таблицу; слабоструктурированные динамические данные — храним JSON в строке; ...). Работу с такими штуками на уровне linq не выразить.
Во-вторых, поведение часто бывает динамическим. Давайте разовьём пример с заказами. Пусть всего в системе у заказа может быть пять способов его оплаты. Кроме того, заказ может входить в некие программы поставок двух типов (а может и не входить). Грубо говоря, это нам даёт максимум 3х5=15 вариаций поведения на каждый аспект заказа. Окей, происходит бизнес-операция — изменение заказа, включающее в себя изменение способа оплаты и перенос в программу поставок. Рассмотрим один из аспектов — стоимость заказа. Стоимость определяется ценой (которая зависит от способа оплаты) и скидкой (которая зависит от программы поставок). В итоговом запросе мы должны получить что-то вроде
UPDATE order SET
  ...
  Cost = x.Price * quantity * y.discount,
  ...
FROM Orders
    INNER JOIN PriceListFromPaymentMethod x ON ....
    INNER JOIN SupplyPrograms y ON ...
    ...

Это и есть одна из 15 вариаций, и только для свойства Cost.

Когда всё на сущностях, то всё просто. Способ определения цены — это абстракция (некая стратегия), способ определения скидки — тоже. Имплементации смотрят на сущность заказа и вычисляют значение по соответствующему алгоритму. Первая стратегия выдаёт "используйте цену 1000", вторая — "используйте скидку 10%". Код, выполняющий операцию, посчитал итоговую стоимость (900), проставил её в сущность. По другим аспектам отработали свои стратегии, заполнились остальные свойства. В итоге свойства сущности были изменены как надо, дёргаем DAL, он всё сохранил, красота.
Теперь как это провернуть на linq? Вижу два варианта.
а) Какие-то динамические запросы... Каждая стратегия вместо того чтобы просто "взять и посчитать" будет куда-то добавлять свою часть запроса. В итоге будет построен какой-то огромный запрос, которые таки да, одной операцией всё пересчитает и обновит. Но зачем эта промежуточная модель? Попробуйте её отладить...
б) Можно апдейтить свойства по одному — одним запросом обновили цену заказа, другим — другое свойство. Будет ли это просто, понятно и эффективно? Тоже сомневаюсь. Какие там ещё проблемы всплывут в конкурентной среде? Теоретически ведь можем в разных запросах использовать одни и те же данные — значит уже нужен repeatable read...
Brainbench transcript #6370594
Отредактировано 04.10.2017 7:59 Poul_Ko . Предыдущая версия .
Re[19]: Блокировки в бизнес-слое
От: Sinclair Россия https://github.com/evilguest/
Дата: 05.10.17 03:19
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

S>>Хотелось бы убедительный пример.

P_K>100%-но убедительный пример привести не смогу, так как конкретно это технологией не пользовался и реального опыта её применения не имею.
Не-не-не. Вы мне не linq пример приведите, в пример логики, которая "плохо соответствует таблицам".
P_K>Но могу описать на основании каких представлений у меня сложилось озвученное мнение.
P_K>Во-первых, то что бизнес-сущности не всегда один-к-одному соответствуют таблицам я думаю вы согласитесь. В простейших случаях это следствие преднамеренной денормализации (храним список Id строкой, не охота городить таблицу; слабоструктурированные динамические данные — храним JSON в строке; ...).
Откуда в бизнес-логике взялся JSON? Это же всего лишь представление — оно появляется только при взаимодействии с другими системами.
P_K>Во-вторых, поведение часто бывает динамическим. Давайте разовьём пример с заказами. Пусть всего в системе у заказа может быть пять способов его оплаты. Кроме того, заказ может входить в некие программы поставок двух типов (а может и не входить). Грубо говоря, это нам даёт максимум 3х5=15 вариаций поведения на каждый аспект заказа. Окей, происходит бизнес-операция — изменение заказа, включающее в себя изменение способа оплаты и перенос в программу поставок. Рассмотрим один из аспектов — стоимость заказа. Стоимость определяется ценой (которая зависит от способа оплаты) и скидкой (которая зависит от программы поставок). В итоговом запросе мы должны получить что-то вроде
P_K>
P_K>UPDATE order SET
P_K>  ...
P_K>  Cost = x.Price * quantity * y.discount,
P_K>  ...
P_K>FROM Orders
P_K>    INNER JOIN PriceListFromPaymentMethod x ON ....
P_K>    INNER JOIN SupplyPrograms y ON ...
P_K>    ...
P_K>

P_K>Это и есть одна из 15 вариаций, и только для свойства Cost.


P_K>Когда всё на сущностях, то всё просто. Способ определения цены — это абстракция (некая стратегия), способ определения скидки — тоже. Имплементации смотрят на сущность заказа и вычисляют значение по соответствующему алгоритму. Первая стратегия выдаёт "используйте цену 1000", вторая — "используйте скидку 10%". Код, выполняющий операцию, посчитал итоговую стоимость (900), проставил её в сущность. По другим аспектам отработали свои стратегии, заполнились остальные свойства. В итоге свойства сущности были изменены как надо, дёргаем DAL, он всё сохранил, красота.

Давайте напишем для начала всё это на чистом С#.
Вот у нас, допустим, класс Order. У него есть операция CalculateCost(). Как она устроена?
Мы будем делать 15 наследников класса Order c перегрузками?
Или у нас будут свойства PaymentMethod и SupplyProgram, у которых методы GetPrice() и GetDiscount() — виртуальные?
Давайте детализировать.
P_K>Теперь как это провернуть на linq? Вижу два варианта.
P_K>а) Какие-то динамические запросы... Каждая стратегия вместо того чтобы просто "взять и посчитать" будет куда-то добавлять свою часть запроса. В итоге будет построен какой-то огромный запрос, которые таки да, одной операцией всё пересчитает и обновит. Но зачем эта промежуточная модель? Попробуйте её отладить...
Ничего сложного.
Предположим, к примеру, что PaymentMethod — это один из пяти well-known типов, т.е. покрыт перечислением (и добавлять новый метод без перекомпиляции мы не планируем).
У нас есть где-то на более-менее корневом уровне сервис получения прайс-листа: GetPriceList(PaymentMethod paymentMethod).
В Linq-мире он возвращает IQueryable<PriceListItem>. Внутри он может быть устроен более-менее как угодно:
{
  switch(paymentMethod)
  {
    case PaymentMethod.Cash: 
      return from db.CashPrices select new PriceListItem(itemId, price);
    case PaymentMethod.Visa: 
      return from db.CCPrices select new PriceListItem(itemId, visaPrice);
    case PaymentMethod.MasterCard:
      return from db.CCPrices select new PriceListItem(itemId, masterCardPrice);
  }
}

То есть у нас тут и разные таблицы, и разные колонки в одной таблице.
В итоге, метод заказа устроен как-то примерно так:
var totalCost = (from pl in GetPriceList(PaymentMethod) join i in Items on pl.itemId equals i.itemId select i.quantity * pl.price).Sum();

И его вычисление вместо нудных N+1 запросов превратится в нормальный join c агрегатом на стороне СУБД. Писать и отлаживать это ещё проще, чем пошаговую логику в традиционном ERP-приложении с Rich ORM и Lazy Load.
При этом производительность будет как минимум на порядок выше.

P_K>б) Можно апдейтить свойства по одному — одним запросом обновили цену заказа, другим — другое свойство. Будет ли это просто, понятно и эффективно? Тоже сомневаюсь. Какие там ещё проблемы всплывут в конкурентной среде? Теоретически ведь можем в разных запросах использовать одни и те же данные — значит уже нужен repeatable read...
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[20]: Блокировки в бизнес-слое
От: Poul_Ko Казахстан  
Дата: 05.10.17 04:39
Оценка:
Здравствуйте, Sinclair, Вы писали:
P_K>>Во-первых, то что бизнес-сущности не всегда один-к-одному соответствуют таблицам я думаю вы согласитесь. В простейших случаях это следствие преднамеренной денормализации (храним список Id строкой, не охота городить таблицу; слабоструктурированные динамические данные — храним JSON в строке; ...).
S>Откуда в бизнес-логике взялся JSON? Это же всего лишь представление — оно появляется только при взаимодействии с другими системами.
Но ведь linq оперирует объектами, напрямую отражающими структуру таблиц?
Предположим что у нас прайс-лист для какого-то одного случая хранится в базе в JSON-строке. Когда я пишу логику на сущностях мне пофиг — DAL достанет этот JSON и переведёт его в объекты, которыми я уже и буду оперировать. А вот когда я пишу логику на linq? Как я смогу достать из этого прайс-листа несколько нужных мне позиций запросом? Вот я о чём.

P_K>>Во-вторых, поведение часто бывает динамическим. Давайте разовьём пример с заказами. Пусть всего в системе у заказа может быть пять способов его оплаты. Кроме того, заказ может входить в некие

S>Давайте напишем для начала всё это на чистом С#.
S>Вот у нас, допустим, класс Order. У него есть операция CalculateCost(). Как она устроена?
S>Мы будем делать 15 наследников класса Order c перегрузками?
Нет, всё не так. Заказ не считает свою цену
S>Или у нас будут свойства PaymentMethod и SupplyProgram, у которых методы GetPrice() и GetDiscount() — виртуальные?
Опять мимо. Я же написал, будут использоваться стратегии. Как-то так:
public interface IPricingStrategy {    // Будет реализация, которая смотрит на заказ и по его свойствам определяет откуда брать цену, плюс простая реализация для тестов
  decimal GetPrice(Order order);
}

public interfact IDiscountingStrategy {    // Будет реализация, которая смотрит на заказ и по его свойствам определяет откуда брать скидку, плюс простая реализация для тестов
  decimal GetDiscountPercents(Order oreder);
}

// Пересчёт стоимости заказа после изменений
private void RecalcOrderCost(Order order) {
  var price = _pricingStrategy.GetPrice(order);
  var discount = _discountingStrategy.GetDiscountPercents(order) / 100M;
  order.Cost = order.Quantity * price * (1M - discount);
}

Нетестовые реализации стратегий могут быть разные — где-то в виде swith или набора if, где-то более изощрённо-динамически, но не суть.

S>Давайте детализировать.

S>
S>var totalCost = (from pl in GetPriceList(PaymentMethod) join i in Items on pl.itemId equals i.itemId select i.quantity * pl.price).Sum();
S>

S>И его вычисление вместо нудных N+1 запросов превратится в нормальный join c агрегатом на стороне СУБД. Писать и отлаживать это ещё проще, чем пошаговую логику в традиционном ERP-приложении с Rich ORM и Lazy Load.
Но опять же, это будет отдельный запрос, который вернёт сразу сумму. А стоимость нужно будет потом проапдейтить в заказе — другим запросом.
Точно так же будет и в моём случае — реализация стратегии сбегает в базу, посчитает стоимость и отдаст сразу в виде decimal. А не в виде IQueriable<что-то>, что нужно ещё потом правильно сджинить. Кроме того, стратегия может бросить исключение, например, "для товара нет цены в прайс-листе", что является предсказуемой бизнес-ситуацией и обрабатывается. А вот как это выяснить для IQueriable?
S>При этом производительность будет как минимум на порядок выше.
Будет выше — скорее всего. На порядок или нет — спорить не готов.

Вернёмся к исходной проблеме. Мы хотим обеспечить ситуацию, чтобы на время обновления заказа использованный прайс-лист не мог быть изменён.
Что в вашем случае, что в моём поведение с т.з. базы одинаково — будет последовательность запросов:
1) найти заказ
2) найти цены
3) проапдейтить заказ
Ни тот ни другой подход не обеспечат того, что между 2 и 3 использованные в шаге 2 сущности не поменяются. Поэтому нет смысла продолжать спорить в рамках этой темы о том как лучше выбирать данные — через linq или нет.
Brainbench transcript #6370594
Re[21]: Блокировки в бизнес-слое
От: samius Япония http://sams-tricks.blogspot.com
Дата: 05.10.17 04:49
Оценка:
Здравствуйте, Poul_Ko, Вы писали:

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


Если хранить не текущую цену, а историю изменения цен, а при формировании Order выбирать последнюю установленную цену на момент формирования, то станет при любом пододе (linq или не linq) сложно накосячить. Да и тема блокировки в бизнес-слое исчезает.
Re[21]: Блокировки в бизнес-слое
От: Sinclair Россия https://github.com/evilguest/
Дата: 05.10.17 04:57
Оценка:
Здравствуйте, Poul_Ko, Вы писали:
P_K>Но ведь linq оперирует объектами, напрямую отражающими структуру таблиц?
Да.
P_K>Предположим что у нас прайс-лист для какого-то одного случая хранится в базе в JSON-строке. Когда я пишу логику на сущностях мне пофиг — DAL достанет этот JSON и переведёт его в объекты, которыми я уже и буду оперировать. А вот когда я пишу логику на linq? Как я смогу достать из этого прайс-листа несколько нужных мне позиций запросом? Вот я о чём.
Во-первых, как минимум — не хуже, чем в "логике на сущностях".
Во-вторых, если СУБД умеет работать с JSON, то у вас есть шанс подпилить маппер так, чтобы парсинг выполнялся на стороне СУБД.
В-третьих, если СУБД не умеет работать с JSON, но вы всё равно храните данные в нём, и при этом вам надо работать с отдельными позициями, то вам пора уволить архитектора, пока он вас не загнал в банкротство.

P_K>Опять мимо. Я же написал, будут использоваться стратегии. Как-то так:

P_K>[c#]
P_K>public interface IPricingStrategy { // Будет реализация, которая смотрит на заказ и по его свойствам определяет откуда брать цену, плюс простая реализация для тестов
P_K> decimal GetPrice(Order order);
P_K>}
О
P_K>public interfact IDiscountingStrategy { // Будет реализация, которая смотрит на заказ и по его свойствам определяет откуда брать скидку, плюс простая реализация для тестов
P_K> decimal GetDiscountPercents(Order oreder);
P_K>}
Ок. Откуда берутся эти стратегии? Приведите реализацию какой-нибудь из этих стратегий. Пока что всё ещё непонятно, что там будет за код, и почему его трудно переписать на linq.

P_K>Но опять же, это будет отдельный запрос, который вернёт сразу сумму. А стоимость нужно будет потом проапдейтить в заказе — другим запросом.

P_K>Точно так же будет и в моём случае — реализация стратегии сбегает в базу, посчитает стоимость и отдаст сразу в виде decimal.
Хорошо, если так. Пока я не увижу кода реализации, я не могу понять, что вы имеете в виду под "сбегает в базу".

P_K>А не в виде IQueriable<что-то>, что нужно ещё потом правильно сджинить. Кроме того, стратегия может бросить исключение, например, "для товара нет цены в прайс-листе", что является предсказуемой бизнес-ситуацией и обрабатывается. А вот как это выяснить для IQueriable?

В целом — точно так же. В идеале, "стратегия" будет просто скомпилирована в SQL, который выполнится прямо в базе. И вместо отдачи decimal, который на клиенте нужен только для того, чтобы тут же отдать его обратно в базу, вернёт expression, который можно использовать в update order set TotalCost = <>.

S>>При этом производительность будет как минимум на порядок выше.

P_K>Будет выше — скорее всего. На порядок или нет — спорить не готов.
Тут всё очень просто: как правило, такие "маленькие" запросы на стороне сервера не стоят почти ничего. Основная стоимость — это roundtrip. Когда в заказе примерно 10 позиций, как раз и получаем примерно десятикратное улучшение производительности.

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

P_K>Что в вашем случае, что в моём поведение с т.з. базы одинаково — будет последовательность запросов:
P_K>1) найти заказ
P_K>2) найти цены
P_K>3) проапдейтить заказ
P_K>Ни тот ни другой подход не обеспечат того, что между 2 и 3 использованные в шаге 2 сущности не поменяются. Поэтому нет смысла продолжать спорить в рамках этой темы о том как лучше выбирать данные — через linq или нет.
Я вам попробую ещё раз объяснить, что ключ к успеху — это
1. Использовать возможности СУБД для изоляции транзакций. То есть следить за тем, чтобы все три пункта выполнялись в рамках одной транзакции с подходящим уровнем изоляции.
2. Минимизировать количество раундтрипов между СУБД и миддл-тир, потому что они увеличивают время выполнения транзакции и, соответственно, увеличивают шансы напороться на ожидание или дедлок.
3. Традиционным ответом на эти два требования является написание хранимок. К сожалению, у хранимок есть несколько фундаментальных проблем:
3.1. Плохие возможности по декомпозиции. Введение table-valued функций несколько помогает, но даже с ними современный SQL на поколения отстаёт от полноценных языков программирования.
3.2. Проблемы по синхронизации версий клиента и сервера. Можно запросто напороться на ситуацию, когда версия кода в миддл-тир не совпадает с версией кода в хранимках, и происходят трудноуловимые глюки.
3.3. Проблемы с рефакторингом и отладкой. В отличие от C#, статически гаратировать корректность кода на SQL очень тяжело.
4. Потенциальным решением для этих проблем является linq — как способ порождения SQL кода из C#-кода. В таком варианте ваш код всегда корректен, дружественен оптимизатору СУБД, и при этом его всё ещё может читать и поддерживать живой человек.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.