Две альтернативы хранимым процедурам в облачных сервисах

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

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

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

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

В дополнение к этим базовым возражениям я лично предпочитаю четкое разделение между представлением (presentation), бизнес-моделированием (business mode- ling), реализацией бизнес-логики (business logic) и собственно данными (data).

Последнее из моих возражений субъективно, и я допускаю, что некоторые могут посчитать его просто "дурацким личным заскоком". Но первые два возражения представляют реальные проблемы. Подумайте сами — многие ли из вас вынуждены использовать приложения, работающие с Oracle, которые легко можно было бы перевести на работу с MySQL, если бы не хранимые процедуры? Вы будете вынуждены платить Oracle немалые деньги — а все из-за того, что ваше приложение использует хранимые процедуры!

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

Чтобы сохранить программную логику на уровне сервера приложений, но при этом одновременно обеспечить защиту целостности транзакций в многосерверной среде, вам необходимо либо реализовать защиту от феномена "грязной записи" (dirty writes)1, либо создать блокировку в базе данных.

В главе 3 книги [2] я подробно описал политику управления транзакциями для систем, написанных на Java и управляющих логикой транзакций на уровне сервера приложений. Одним из приемов, которые я описал в упомянутой книге и который в общем случае настоятельно рекомендую применять, независимо от общей архитектуры вашего приложения, благодаря скоростным преимуществам этого метода, является использование временной метки последнего обновления (last update time- stamp) и агента модификации (modifying agent) при выполнении обновлений.

Логика бронирования из хранимой процедуры, в сущности, представляет собой обновление таблицы бронирований (booking):

UPDATE booking SET customer = ? WHERE booking_id = ?;

Если вы добавите поля last_update_timestamp и last_update_user, SQL будет эффективнее работать в многосерверной среде. Пример использования этого приема показан в листинге 4.4.

Листинг 4.4. Добавление полей last_update_timestamp и last_update_user

с целью повышения эффективности работы SQL в многосерверной среде

UPDATE booking

SET customer = ?, last_update_timestamp = ?, last_update_user = ?

WHERE booking_id = ? AND last_update_timestamp = ? AND last_update_user = ?;

1 В идеальном случае результат выполнения транзакции не должен зависеть от остальных транзакций, сколько бы транзакций ни выполнялось параллельно. Но, к сожалению, этот идеальный случай накладывает сильные ограничения на параллельную обработку, практически выстраивая транзакции в очередь. Однако в большинстве случаев такая строгость не нужна, и поэтому были введены так называемые "уровни изоляции" (Isolation Level), которые определяют степень параллелизма выполнения транзакций. Чем ниже уровень изоляции, тем выше степень параллелизма и тем больше риск "неправильного" выполнения транзакции. В стандарте ANSI SQL уровни изоляции привязаны к четырем "феноменам" — нарушениям изолированности транзакций: грязная запись (dirty write), грязные чтения (dirty reads), неповторяющиеся чтения (Non-repeatable Read или Fuzzy Read) и фантомы (Phantom). В частности, феномен грязной записи можно описать так: представьте себе, что некоторая транзакция T1 модифицирует некий элемент данных. После этого другая транзакция T2 тоже модифицирует этот элемент данных, и делает это до того, как транзакция T1 выполнит COMMIT или ROLLBACK. Если T1 или T2 после этого выполнит ROLLBACK, то становится непонятным, каким должно быть корректное значение данных. Подробнее об этом см. http://www.rsdn.ru/article/db/deadlocks.xml. — Прим. перев.

В данной ситуации первый клиент попытается забронировать номер в отеле на указанную дату, и его транзакция будет успешной. Теперь представим себе, что второй клиент пытается обновить эту запись. Этот второй клиент не получит совпадений, потому что временная метка (timestamp), которую он прочитает, как и идентификатор пользователя (user ID), не совпадут со значениями, обновленными первым клиентом. Его транзакция завершится неудачей, потому что он увидит, что не обновлено ни одной записи и появилось сообщение об ошибке. И никакого больше двойного бронирования!

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

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

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

Еще один вариант заключается в создании поля, которое позволит управлять вашими блокировками. Например, в таблицу room можно добавить два дополнительных столбца для целей бронирования номеров: locked_by и locked_timestamp. Прежде чем начинать транзакцию для бронирования номера, необходимо обновить таблицу room и подтвердить изменение (commit). После того как транзакция бронирования завершится, снимите блокировку, обнулив эти поля перед подтверждением этой транзакции.

Поскольку этот подход требует проведения двух транзакций над базой данных, вы более не будете выполнять бронирование номера как единую и неделимую транзакцию (atomic transaction). Следовательно, вы рискуете оставить открытую блокировку, которая не позволит никому бронировать номера ни на какие даты. Существуют два способа решения этой проблемы.

± Номер считается разблокированным не только когда поля locked_by и locked_timestamp имеют значение NULL, но и когда поле locked_timestamp слишком долго остается открытым.

± При обновлении блокировки в конце транзакции бронирования номера использовать поля locked_by и locked_timestamp в составе предложения WHERE. Таким образом, если кто-то другой "украдет" у вас блокировку, вы в результате всего лишь выполните откат вашей транзакции.

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

Источник: Риз Дж., Облачные вычисления: Пер. с англ. — СПб.: БХВ-Петербург, 2011. — 288 с.: ил.

Вы можете следить за любыми ответами на эту запись через RSS 2.0 ленту. Вы можете промотать до конца и оставить ответ. Pinging в настоящее время не допускается.

Оставьте отзыв

XHTML: Вы можете использовать следующие теги: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

 
Rambler's Top100