И снова хочу поделиться success-story.
Готовились к черной пятнице и проводили нагрузочные тестирования. Заметили блокировки при большом числе web-вызовов по операциям резервирования по заказам на продажу на InventSum. Нагрузка была такая, что даже номерную серию SalesId пришлось на последовательности в базе MSSQL перевести. Пришла в голову идея таки включить OCC (оптимистические блокировки) на InventSum.
Если посмотреть код на
InventUpdateOnhand.ttsNotifyPreCommit, то можно заметить, что
COMMIT выполняется логично, но крайне избыточно.
- Вставляются новые строки InventSumDelta -> InventSum (причем только ключевые поля без данных). Если строка DELTA-таблицы одна, то строка InventSum даже вставляется (см.п 3)
- Блокируются строки InventSum по наличию в InventSumDelta
- Далее идет обновление строк InventSum. Тут 2 варианта
- если строка InventSumDelta была одна, то читаем пессимистически ее и выполняем ей суммирование количеств из InventSumDelta и WRITE (Insert для той самой строки из п.1). Тут без пессимистической блокировки было не обойтись
- если строк InventSumDelta по нашей транзакции несколько, то ту строится динамический SQL и обновляются строки InventSum на основании группового запроса в InventSumDelta. Тут красиво читаются данные из InventSum и обновление происходит как поле базы += значение из InventSumDelta. InventSum.RecVersion не обновляется. В базе на измененные строки наложена блокировки. Никто не может теперь их читать. Все правильно
- Далее дополнительно проводится проверка на отрицательные остатки. И если что-то не так, то идет откат транзакции
Схема красивая рабочая с минимальными блокировками в конце комита, но ... Хочется включить оптимистические блокировки. Если в лоб на таблице InventSum включить OCC, то получаем проблему, когда групповой запрос не обновляет RecVersion, а единичный обновляет измененные строки из-за того, что вычитал старые данные, обновил в памяти кол-во и не упал на обновлении из-за совпадающего RecVersion. Ну и дополнительные выборки с пессимистиком, только чтобы залочить строки перед групповым обновлением.
Чего удалось достичь:
- При вставке новых записей InventSumDelta->InventSum вставляются всегда, даже если строка в InventSumDelta одна. Причем вставляются уже с данными для одной строки Delta-таблицы.
X++:
if (inventSumDeltaCnt == 1)
{
select ItemId, InventDimId,
#InventSumDeltaMax
from inventSumDelta
group by ItemId, InventDimId
where inventSumDelta.ttsId == this.ttsId() &&
inventSumDelta.IsAggregated == NoYes::No
notexists join inventSum
where inventSum.ItemId == inventSumDelta.ItemId &&
inventSum.InventDimId == inventSumDelta.InventDimId;
}
- Строки вообще не блокируются перед обновлением. По крайней мере при резервировании товара
X++:
protected void lockInventSum()
{
InventSum inventSum;
InventSumDeltaDim inventSumDeltaDim;
/*
if (!this.parmDoOnhandCheck()) //!doSummarizedOnhandCheck && !doDirectOnhandCheck && !checkOnHandForWHSItems
*/
if (!doSummarizedOnhandCheck && !doDirectOnhandCheck)
{
return;
}
- При обновлении работает исключительно групповая процедура (прямой SQL) на основании групповой подвыборки InventSumDelta с оптимизациями
- - для одной строки InventSumDelta, которая была вставлена ранее вызов вообще не выполняется
- - если строка одна и обновляется, то подвыборка-InventSumDelta не выполняется. Все есть табличной переменной InventSumDelta и в динамический SQL подставляются константы
- - обновлятся RecVersion в строках InventSum
RecVersion = ' = FLOOR(RAND()*2147483647)+1'
В итоге блокировки InventSum ушли и подсистема резервирования (по сути любые операции обновления InventSum) стала выдерживать гораздо большие нагрузки от веб-сервисов. Если случаются коллизии при резервировании, когда 2 клиента пытаются одновременно зарезервировать последнюю единицу товара, то транзакция откатится на этапе проверки отрицательных остатков (как было и раньше).
Огромная торговая компания
розница + интернет магазин в черную пятницу отлично себя чувствовали при онлайн резервировании.