
Июль/Август 2003
Профессионалу администратору
Том Кайт
Корпорация Oracle
Разработка успешных приложений для СУБД Oracle
(Developing Successful Oracle Applications)
(Часть III)
Источник: журнал Oracle Magazine, раздел "Articles online only", 2001,
/oramag/webcolumns/2001/4826_Chap01.pdf
[ От редакции "Oracle Magazine/Русское Издание":
мы продолжаем начатую в двух предыдущих номерах журнала публикацию
фрагментов из первой главы "Разработка успешных приложений для СУБД Oracle"
книги Тома Кайта ("Эксперт один на один с системами
баз данных Oracle") в переводе
А.П.Соколова (РДТЕХ, Протвино):
Полностью перевод книги Т.Кайта под названием "Oracle для профессионалов.
Книга 1. Архитектура и основные особенности" выпущен издательством
"ДиаСофт", которое приобрело у изд.
Wrox Press права на перевод и публикацию книги "Expert One on One: Oracle" ,
Wrox Press, 2001, ISBN 1861004826
(
http://www.wrox.com/ACON11.asp?WROXEMPTOKEN=226473Z0ingTdnhkSEinQ3g0kU&ISBN=1861004826).
Публикация в нашем журнале отрывков из первой главы "Разработка успешных приложений для СУБД Oracle"
осуществляется по согласованию и с согласия изд."ДиаСофт".]
Содержание I и II частей:
- Введение. “Разработка успешных приложений для СУБД Oracle”
- Мой подход
- Подход к СУБД как к “черному ящику”
- Как нужно (и как нельзя) разрабатывать приложения для СУБД Oracle
- Понимание архитектуры СУБД Oracle
- Не выполняйте длительные транзакции в среде многопотокового сервера
- Используйте переменные связывания
- Понимание управления конкурентным доступом
Содержание III части:
- *Реализация механизма блокирования
- *Многоверсионность
В следующих выпусках:
- Независимость приложений от СУБД?
- Влияние стандартов
- Функциональные возможности и функции
- Простое решение проблем
- Открытость
- Как я заставляю СУБД работать быстрее
- Отношения АБД с разработчиками
- Заключение
Реализация механизма блокирования
В СУБД блокировки используются для гарантированного обеспечения, что в любое произвольное время не более одной транзакции модифицирует любой заданный набор данных. По существу, это – механизм, который позволяет работать в условиях конкурентного доступа; например, без какой-либо модели блокирования, запрещающей одновременное обновление одной и той же строки, многопользовательский доступ к базе данных будет невозможным. Однако если блокировки используются слишком часто или неправильно, они могут фактически запретить конкурентный доступ. Если вы или СУБД заблокируете ненужные данные, то некоторые пользователи не смогут параллельно выполнять свои операции. Таким образом, понимание того, что такое блокирование и как оно работает в вашей СУБД, имеет важное значение для разработки масштабируемых корректных приложений.
Важно также понимать, что реализации механизма блокирования в разных СУБД отличаются друг от друга. В одних СУБД используется блокирование на уровне страниц, в других – на уровне строк, в третьих реализациях допускается эскалация блокировок с уровня строк до уровня страниц, в четвертых СУБД используются блокировки для чтения, в пятых для реализации механизма сериализации транзакций используется механизм блокирования, а в шестых – согласованные по чтению представления данных (без блокировок). Эти небольшие различия могут стать причиной больших проблем производительности или явных ошибок в вашем приложении, если вы не понимаете, как работает механизм блокирования.
Стратегию блокирования в СУБД Oracle можно обобщить следующим образом:
- СУБД Oracle блокирует данные на уровне строк только для их модификации. Эскалация блокировок до уровня блоков или таблиц отсутствует.
- СУБД Oracle никогда не блокирует данные только для чтения.
- Писатели данных не блокируют читателей данных. Позвольте повторить: операции чтения не блокируются операциями записи. Это фундаментальное отличие почти от всех СУБД, в которых операции чтения блокируются операциями записи.
- Писатель данных блокируется только тогда, когда другой писатель данных уже заблокировал модифицируемую строку. Читатели данных никогда не блокируют писателей данных.
Эти факты необходимо учитывать во время разработки приложений. И вы должны также понимать, что эта стратегия – уникальная стратегия для СУБД Oracle. Разработчик, который не понимает, как в его СУБД реализован конкурентный доступ, непременно столкнется с проблемами целостности данных (это, в частности, общее явление при переходе разработчика с другой СУБД в СУБД Oracle, и наоборот).
Один из побочных эффектов "неблокируемого" подхода в СУБД Oracle заключается в том, что если вы действительно хотите гарантированно обеспечить доступ к строкам единственного пользователя, то вы, как разработчик, должны сами немного поработать над этим. Рассмотрим следующий пример. Разработчик демонстрировал мне программу календарного планирования ресурсов (конференц-залов, проекторов и т.п.), которую он только что разработал и занимался процессом ее внедрения. В приложении было реализовано бизнес-правило, предотвращающее выделение ресурса более чем одному человеку. То есть в приложении было специально запрограммирована проверка: никакой пользователь не получит ранее распределенный временной интервал (по крайней мере, разработчик думал, что он сделал это). Этот код запрашивал таблицу с календарным планом резервирования (schedules), и если никакие существующие строки не перекрывали данный временной интервал, вставлял новую строку. Итак, разработчик в основном занимался двумя таблицами:
create table resources ( resource_name varchar2(25) primary key, ... );
create table schedules ( resource_name varchar2(25) references resources,
start_time date,
end_time date );
И перед, скажем, резервированием конференц-зала (:room_name),
приложение должно было выполнить следующий запрос:
select count(*)
from schedules
where resource_name = :room_name
and (start_time between :new_start_time and :new_end_time
or
end_time between :new_start_time and :new_end_time)
Это кажется простым и "пуленепробиваемым" (во всяком случае для разработчика): если возвращаемое количество строк в заданном временном интервале было равно нулю, конференц-зал ваш. Если возвращалось ненулевое значение, вы не могли резервировать зал на этот промежуток времени. Я провел очень простое тестирование, чтобы показать разработчику ошибку, которая могла произойти во время эксплуатации программы. Такие ошибки очень сложно локализовать, а после локализации некоторые убеждены, что это явная ошибка в СУБД.
Все, сделанное мною, заключалось в том, что я посадил за следующий терминал другого пользователя. Они оба открыли один и тот же экран, затем при счете до трех каждый нажал кнопку Go и попробовал зарезервировать один и тот же зал точно на одно и то же время. Оба они получили резервирование – логика, которая превосходно работает в автономном режиме, сбивается в многопользовательском окружении. В данном случае проблема заключалась в неблокируемом чтении в СУБД Oracle. Никакой сеанс никогда не блокирует другой сеанс. Оба сеанса просто выполняли показанный выше запрос, а потом выполнялась программа для резервирования зала. Они оба могли выполнять запрос для просмотра таблицы с календарным планом резервирования, даже если другой сеанс уже начал модифицировать эту таблицу (изменения не видны другому сеансу до фиксации транзакции, когда уже будет слишком поздно). Они не пытались модифицировать одну и ту же строку в таблице
schedules, поэтому они не должны были блокировать друг друга, следовательно, бизнес-правило не могло исполнять то, для чего оно предназначалось.
Разработчику нужен был способ принудительного соблюдения бизнес-правила в многопользовательском окружении, способ, гарантирующий резервирование данного ресурса в данный момент времени только одним пользователем. В нашем случае решение заключалось в введении небольшой сериалиазции. До выполнения функции count(*), показанной выше, разработчик сначала должен выполнить:
select * from resources where resource_name = :room_name FOR UPDATE;
Немного раньше в главе мы обсуждали пример, когда использование предложения FOR UPDATE приводило к возникновению проблем, но здесь оно помогает реализовать бизнес-правило: здесь мы блокируем ресурс (зал) для резервирования непосредственно перед его резервированием. Заблокировав ресурс, мы пробуем зарезервировать его,
- это гарантия того, что никто другой не будет одновременно модифицировать план резервирования данного ресурса. Все должны ждать до тех пор, пока мы не зафиксируем транзакцию. Возможность перекрытия временных интервалов исчезает. Разработчик должен понимать, что в многопользовательском окружении он должен иногда использовать методы, похожие на те, которые используются в многопотоковом программировании. Предложение FOR UPDATE в данном случае работает как семафор. Оно сериализует доступ к конкретным строкам в таблицах ресурсов – гарантируя, что никто другой не сможет модифицировать их одновременно.
При этом сохраняется возможность параллельной работы, так как потенциально можно иметь тысячи резервируемых ресурсов: мы запрещаем одновременную модификацию только одного ресурса. При ручном блокировании данных вы, как правило, действительно собираетесь изменить их. Вы должны понимать, когда нужно это делать, но, возможно, еще важнее понимать,, когда не нужно этого делать (пример, когда не нужно это делать, приведен ниже). Кроме того, блокирование ресурсов не запрещает другим пользователям читать заблокированные данные, как это делается в некоторых других СУБД, поэтому будет сохраняться хорошая масштабируемость.
Подобные проблемы имеют большое значение при переносе приложений из одной СУБД в другую (я вернусь к этой теме немного позднее) – на этом разработчики иногда "спотыкаются". Например, если вы знаете по опыту работы в другой СУБД, что писатели блокируют читателей и наоборот, то у вас может появиться уверенность, что вы защищены от проблем целостности данных. Отсутствие конкурентного доступа – один из способов защиты, так работают многие другие СУБД. В СУБД Oracle уровень конкурентного доступа очень высокий, и вы должны быть осведомлены об этом факте и учитывать его в своих приложениях (или нести ответственность за последствия).
В 99 процентах времени работы блокирование абсолютно прозрачно и вы не должны заниматься им. Но в 1 оставшемся проценте вы должны научиться распознавать возможные проблемы. Для решения таких проблем нет простого контрольного списка вопросов типа: "Если вы сделали это, вам нужно сделать то". Это вопрос понимания поведения вашего приложения в многопользовательском окружении и его поведения в вашей СУБД.
Многоверсионность
Эта тема очень тесно связана с управлением конкурентным доступом, так как многоверсионность создает основу для механизма управления конкурентным доступом в СУБД Oracle. В СУБД Oracle используется многоверсионная, согласованная по чтению модель конкурентного доступа. В главе 3, "Блокирование и конкурентность", мы более подробно рассмотрим технические аспекты, сейчас же для нас важно то, что этот механизм используется в СУБД Oracle для обеспечения:
- согласованных по чтению запросов
– запросов, которые выдают согласованные результаты относительно момента времени;
- неблокируемых запросов
– запросы никогда не блокируются писателями данных, как это делается во многих других СУБД.
В СУБД Oracle есть два очень важных понятия. Термин “многоверсионность”
в основном связан с тем фактом, что СУБД Oracle способна поддерживать в базе данных
множественные версии данных. Если вы понимаете, как работает многоверсионность, вы всегда будете понимать, как СУБД будет реагировать на ваши запросы. Перед немного более подробным изучением механизмов многоверсионности в СУБД Oracle рассмотрим самый простой способ демонстрации многоверсионности в СУБД Oracle:
tkyte@TKYTE816> create table t
2 as
3 select * from all_users;
Table created.
tkyte@TKYTE816> variable x refcursor
tkyte@TKYTE816> begin
2 open :x for select * from t;
3 end;
4 /
PL/SQL procedure successfully completed.
tkyte@TKYTE816> delete from t;
18 rows deleted.
tkyte@TKYTE816> commit;
Commit complete.
tkyte@TKYTE816> print x
USERNAME USER_ID CREATED
------------------------------ ---------- ---------
SYS 0 04-NOV-00
SYSTEM 5 04-NOV-00
DBSNMP 16 04-NOV-00
AURORA$ORB$UNAUTHENTICATED 24 04-NOV-00
ORDSYS 25 04-NOV-00
ORDPLUGINS 26 04-NOV-00
MDSYS 27 04-NOV-00
CTXSYS 30 04-NOV-00
...
DEMO 57 07-FEB-01
18 rows selected.
В этом примере мы создаем таблицу Т, в которую загружаем данные из таблицы ALL_USERS. Открываем курсор для этой таблицы.
Мы не извлекаем данные из этого курсора, мы только открываем его. Имейте в виду, что СУБД Oracle не "отвечает" на запрос, никуда не копирует данные при открытии курсора (представьте, сколько времени потребовалось бы на открытие курсора при копировании миллиарда строк таблицы). Курсор открывается немедленно, и именно он отвечает на запрос. Другими словами, курсор будет читать данные из таблицы только тогда, когда вы будете извлекать их из курсора.
Затем в том же сеансе (или, возможно, в другом) мы удаляем из таблицы Т все данные. Мы даже пойдем так далеко, что зафиксируем транзакцию с этим удалением (COMMIT). Строки таблицы потеряны. Но в действительности они доступны через курсор. Дело в том, что результирующее множество, которое возвращается к нам, предопределено на момент времени открытия курсора (выполнение оператора OPEN). Во время открытия курсора мы не "прикасались" ни к одному блоку данных в таблице, но ответ уже был зафиксирован "в камне". У нас нет никаких способов узнать ответ до извлечения данных, но с точки зрения нашего курсора он будет неизменным. Не потому, что СУБД Oracle во время открытия курсора копирует все наши данные в какое-то другое место, на самом деле это делается во время выполнения оператора delete; наши данные сохраняются в области данных, которая называется сегментами отката (rollback segments).
В этом заключается суть согласованности данных по чтению, и если вы не понимаете концепций многоверсионности в СУБД Oracle и того, на что она влияет, вы либо не сможете воспользоваться всеми преимуществами СУБД Oracle, либо не сможете написать корректное приложение СУБД Oracle (в котором гарантированно обеспечивается целостность данных).
Рассмотрим последствия многоверсионных, согласованных по чтению запросов и неблокируемого чтения. Если вы не знакомы с многоверсионностью, то, что вы увидите ниже может оказаться неожиданным. Для простоты предположим, что в данном примере в читаемой нами таблице каждая строка хранится в отдельном блоке базы данных (наименьшая единица хранения в базе данных) и выполняется полный просмотр таблицы.
Мы будем запрашивать простую таблицу accounts (счета).
Она содержит балансы счетов (account_balance) в банке и имеет очень простую структуру:
create table accounts
( account_number number primary key,
account_balance number
);
В действительности эта таблица может содержать сотни тысяч строк, но для простоты мы будем рассматривать таблицу с четырьмя строками (более подробно мы рассмотрим этот пример в главе 3, "Блокирование и конкурентность"):
|
Строка |
Номер счета (Account Number) |
Баланс счета (Account Balance) |
|
1 |
123 |
$500.00 |
|
2 |
234 |
$250.00 |
|
3 |
345 |
$400.00 |
|
4 |
456 |
$100.00 |
Подготовим отчет о балансе банка в конце банковского рабочего дня. Это очень простой запрос:
select sum(account_balance) from accounts;
Конечно, в этом примере ответ очевиден: $1250. Но что случится, если мы прочитаем строку 1, а во время чтения строк 2 и 3 "автоматический кассир" (Automated Teller Machine) сгенерирует транзакцию, которая перечислит $400 со счета 123 на счет 456? Наш запрос подсчитает $500 в строке 4 и выдаст ответ: $1650, не так ли? Конечно, этого не должно быть – никогда такой суммы значений в столбце account_balance не было. Вы должны понимать, каким образом СУБД Oracle избегает таких ситуаций и как методы СУБД Oracle отличаются от других СУБД.
Практически в каждой другой СУБД, если вы хотите получить ‘согласованный’ и "корректный" ответ на этот вопрос, то должны либо блокировать всю таблицу на время вычисления суммы, либо блокировать строки во время их чтения. Это позволило бы обеспечить защиту от изменений ответа во время его получения. Если вы блокируете таблицу перед выполнением запроса, то получите ответ по значениям данных на момент времени начала запроса. Если вы блокируете данные по мере их чтения (обычно называется разделяемой блокировкой чтения, которая запрещает обновление, но все читатели имеют доступ к данным), то получите ответ по значениям данных на момент времени завершения запроса. Оба этих способа блокирования существенно ограничивают конкурентный доступ. Блокировка таблицы запрещает во время выполнения запроса какие-либо обновления таблицы. Блокировка данных "по мере их чтения" запрещает обновление прочитанных данных и фактически может привести к тупиковым ситуациям.
Ранее я сказал, что если вы не понимаете концепций многоверсионности в СУБД Oracle, вы не сможете воспользоваться всеми преимуществами СУБД Oracle. Здесь представлена одна из причин этого. СУБД Oracle использует многоверсионность для получения такого ответа на запрос, который существовал на момент времени начала запроса, причем запрос выполняется без блокирования данных (когда наша транзакция перечисления денег с одного счета на другой обновляла строки 1 и 4, эти строки блокировались для других писателей, но не блокировались для других читателей, выполнявших аналогичные запросы). В СУБД Oracle отсутствует блокировка “разделяемого чтения”, часто встречающаяся в других СУБД. В СУБД Oracle она не нужна. Все, что препятствует конкурентному доступу и может быть устранено, в СУБД Oracle устранено.
Итак, каким образом СУБД Oracle получает корректный, согласованный ответ ($1250), выполняя чтение без какого-либо блокирования данных, другими словами, без понижения уровня конкурентного доступа? Секрет содержится в транзакционных механизмах, используемых в СУБД Oracle. Всякий раз, когда вы модифицируете данные, СУБД Oracle создает записи в двух разных местах. Одна запись поступает в журнальные файлы (redo logs), в которых СУБД Oracle сохраняет достаточно информации для повторного выполнения транзакций (redo или ‘roll forward’ – прим. пер. иногда называются "откатом вперед" или "накатом" базы данных при ее восстановлении). Для операций вставки сохраняется вся вставляемая строка. Для операций удаления сохраняется сообщение об удалении строки в файле X, блоке Y, участке блока Z. И т.д. Другая запись (undo) поступает в сегмент отката (rollback segment). Если ваша транзакция завершается аварийно и ее требуется “откатить”, СУБД Oracle читает исходные данные из сегмента отката и восстанавливает их. Кроме использования сегментов отката для отката транзакций, СУБД Oracle использует их во время чтения для восстановления блоков на момент времени начала транзакций. Это позволяет правильно читать блоки, заблокированные и измененные другими транзакциями, и получать согласованные, корректные ответы без какого-либо блокирования данных читающим сеансом.
Так, в нашем примере СУБД Oracle выполняет следующие операции:
|
Время |
Запрос |
Транзакция перечисления денег |
|
T1 |
Читает строку 1, сумма = $500. |
|
|
T2 |
|
Обновляет строку 1, накладывает монопольную блокировку на строку 1, запрещающую другие изменения (но не чтения). В строке 1 сейчас $100. |
|
T3 |
Читает строку 2, сумма = $750. |
|
|
T4 |
Читает строку 3, сумма = $1150. |
|
|
T5 |
|
Обновляет строку 4, накладывает монопольную блокировку на строку 4, запрещающую другие изменения (но не чтения). В строке 4 сейчас $500. |
|
T6 |
Читает строку 4, обнаруживает, что строка 4 была модифицирована. Восстанавливает содержимое блока из сегмента отката, которое было во время T1. Из этого блока читает значение $100. |
|
|
T7 |
|
Фиксирует транзакцию. |
|
T8 |
Выдает ответ: $1250. |
|
Во время T6 СУБД Oracle читает строку 4 несмотря на блокировку, наложенную нашей транзакцией. Так в СУБД Oracle реализовано неблокируемое чтение – СУБД проверяет только, изменены ли данные, не обращая внимания на текущую блокировку. Затем она просто извлекает старое значение из сегмента отката и переходит к следующему блоку данных.
Еще один пример многоверсионности: существует несколько версий одной и той же порции информации, соответствующих разным моментам времени. СУБД Oracle может использовать эти "моментальные копии" данных, сделанные в разные моменты времени, для выполнения согласованных по чтению запросов и неблокируемых запросов.
Согласованное по чтению представление данных всегда выполняется на уровне операторов SQL: результаты выполнения любого оператора SQL будут согласованы относительно времени начала его выполнения. Это свойство позволяет вставлять в таблицы предсказуемые наборы данных, используя для этого операторы типа:
for x in (select * from t)
loop
insert into t values (x.username, x.user_id, x.created);
end loop;
Результат выполнения оператора SELECT * FROM T был предопределен тогда, когда запрос начал выполняться. Оператор SELECT не будет видеть никакие новые данные, вставленные оператором INSERT. В противном случае выполнялся бы бесконечный цикл. Такое согласованное чтение обеспечивается для всех операторов, поэтому выполнение следующего оператора INSERT также предсказуемо:
insert into t select * from t;
Этот оператор будет обрабатывать согласованное по чтению представление таблицы T , он не будет видеть строки, которые он сам только что вставил, он будет вставлять только те строки, которые существовали в момент времени начала выполнения оператора INSERT. Во многих СУБД выполнение рекурсивных операторов запрещено, так как невозможно предсказать количество строк, которые могут быть реально вставлены.
Итак, если вы использовали свои знания о реализации согласованности запросов и конкурентном доступе в других СУБД или не имеете реального опыта работы с СУБД, то теперь вы получили представление о том, как важно понимать функционирование этих механизмов в конкретной СУБД.
Продолжение следует
|