WWW.DISSERS.RU

БЕСПЛАТНАЯ ЭЛЕКТРОННАЯ БИБЛИОТЕКА

   Добро пожаловать!

Pages:     | 1 |   ...   | 2 | 3 || 5 | 6 |   ...   | 24 |

«Oracle для профессионалов Том Кайт DiaSoft 2003 торгово-издательский дом Москва • Санкт-Петербург • Киев УДК 681.3. 06(075) Б Б К 32.973.2 К 91 КАЙТ ТОМ К 91 Oracle для профессионалов. Пер. с ...»

-- [ Страница 4 ] --

Блокирование и одновременный доступ разделяемые блокировки ЯОД будут устанавливаться на таблицы ЕМР и DEPT на все время выполнения оператора CREATE VIEW. Мы можем изменять содержимое этих таблиц, но не их структуру. Последний тип блокировок ЯОД — нарушаемые блокировки разбора. Когда сеанс разбирает оператор, блокировка разбора устанавливается на каждый объект, упоминаемый в этом операторе. Эти блокировки устанавливаются, чтобы разобранный и помещенный в кэш оператор был признан недействительным (и выброшен из кэша в разделяемой памяти), если один из упоминаемых в нем объектов удален или изменена его структура. При поиске этой информации особо ценным будет представление DBA_DDL_LOCKS. (Ни одного походящего для этого представления V$ не существует). Представление DBA_DDL_LOCKS строится по более "мистическим" таблицам Х$ и по умолчанию не создается в базе данных. Для установки его и других представлений, связанных с блокировками, выполните сценарий CATBLOCK.SQL из каталога [ORACLE_HOME]/ rdbms/admin. Этот сценарий можно успешно выполнить от имени пользователя SYS. После выполнения этого сценария можно выполнять запросы к указанному представлению. Например, в базе данных с одним пользователем я получил такой результат:

tkyte@TKYTE816> select * from dba_ddl_locks;

session id OWNE RNAME TYPE 8 8 8 8 8 8 SYS DBMS_APPLICATION_INFO SYS DBMS_APPLICATION_INFO SYS DBMS_OUTPOT SYS DBMS_OUTPUT TKYTE TKYTE SYS DATABASE Body Table/Procedure/Type Table/Procedure/Type Body 18 18 mode mode held reqe Null Null Null Null Null Null None None None None None None 6 rows selected.

Вот и все объекты, "блокируемые" моим сеансом. Установлены нарушаемые блокировки разбора на два пакета DBMS_*. Это — побочный эффект использования утилиты SQL*Plus;

она, например, вызывает пакет DBMS_APPLICATION_INFO. В результатах может оказаться несколько строк для одного и того же объекта — это нормально и означает, что в разделяемом пуле сеанс использует несколько объектов, ссылающихся на данный объект. Интересно отметить, что в столбце OWNER этого представления указан не владелец блокировки, а владелец блокируемого объекта. Вот почему в нескольких строках указан пользователь SYS: он владеет этими пакетами, но блокирует их мой сеанс. Чтобы понять, как действует нарушаемая блокировка, создадим и выполним хранимую процедуру Р:

tkyte@TKYTE816> create or replace procedure p as begin null;

end;

2 / Procedure created. tkyte@TKYTE816> exec p PL/SQL procedure successfully completed.

Глава Процедура Р теперь упоминается в представлении DBA_DDL_LOCKS. На нее установлена блокировка разбора:

tkyte@TKYTE816> select * from dba_ddl_locks;

session id OWNER 8 8 8 8 8 8 8 TKYTE SYS SYS SYS SYS TKYTE SYS mode mode held reqe Null Null Null Null Null Null Null None None None None None None None NAME P DBMS_APPLICATION_INFO DBMS_APPLICATION_INFO DBMS_OUTPUT DBMS_OUTPUT TKYTE DATABASE TYPE Table/Procedure/Type Body ТаЫе/Procedure/Type Table/Procedure/Type Body 18 1 rows selected.

Перекомпилируем процедуру и еще раз запросим представление:

tkyte@TKYTE816> alter procedure p compile;

Procedure altered. tkyte@TKYTE816> select * from dba_ddl_locks;

session id OWNER 8 8 8 8 8 8 SYS SYS SYS SYS TKYTE SYS mode mode held reqe Null Null Null Null Null Null None None None None None None NAME DBMS_APPLICATION_INFO DBMS_APPLICATION_INFO DBMS_OUTPUT DBMS_OUTPUT TKYTE DATABASE TYPE Body ТаЫе/Procedure/Type Таblе/Procedure/Type Body 18 6 rows selected.

Теперь строки для процедуры Р в представлении нет — наша нарушаемая блокировка снята. Это представление пригодится разработчикам, когда обнаружится, что какой-то фрагмент кода не компилируется в тестовой среде или среде разработки — он зависает и в конечном итоге попытка завершается неудачно. Это показывает, что этот код использует (фактически — выполняет) какой-то другой сеанс, и представление позволяет понять, какой именно. То же самое происходит с операторами GRANT и другими типами операторов ЯОД, применяемыми к объекту. Нельзя, например, предоставить право на выполнение (EXECUTE) процедуры, работающей в настоящий момент. Для обнаружения потенциально блокирующих и ожидающих снятия блокировки сеансов можно использовать описанный выше метод.

Блокирование и одновременный доступ Защелки и внутренние блокировки Защелки и внутренние блокировки (enqueues) — простейшие средства обеспечения очередности доступа, используемые для координации многопользовательского доступа к общим структурам данных, объектам и файлам. Защелки — это блокировки, удерживаемые в течение очень непродолжительного времени, достаточного, например, для изменения структуры данных в памяти. Они используются для защиты определенных структур памяти, например буферного кэша или библиотечного кэша в разделяемом пуле (эти структуры описаны в главе 2). Защелки обычно запрашиваются системой в режиме ожидания. Это означает, что, если защелку нельзя установить, запрашивающий сеанс прекращает работу ("засыпает") на короткое время, а затем пытается повторить операцию. Другие защелки могут запрашиваться в оперативном режиме, т.е. процесс будет делать что-то другое, не ожидая возможности установить защелку. Поскольку возможности установить защелку может ожидать несколько запрашивающих сеансов, одни из них будут ожидать дольше, чем другие. Защелки выделяются случайным образом по принципу "кому повезет". Сеанс, запросивший установку защелки сразу после освобождения ресурса, установит ее. Нет очереди ожидающих освобождения защелки — есть просто "толпа" пытающихся ее получить. Для работы с защелками Oracle использует неделимые инструкции типа "проверить и установить". Поскольку инструкции для установки и снятия защелок — неделимые, операционная система гарантирует, что только один процесс сможет установить защелку. Поскольку это делается одной инструкцией, то происходит весьма быстро. Защелки удерживаются непродолжительное время, причем имеется механизм очистки на случай, если владелец защелки "скоропостижно скончается", удерживая ее. Эта очистка обычно выполняется процессом PMON. Внутренние блокировки — более сложное средство обеспечения очередности доступа, используемое, например, при изменении строк в таблице базы данных. В отличие от защелок, они позволяют запрашивающему "встать в очередь" в ожидании освобождения ресурса. Запрашивающий защелку сразу уведомляется, возможно ли это. В случае внутренней блокировки запрашивающий блокируется до тех пор, пока не сможет эту блокировку установить. Таким образом, внутренние блокировки работают медленнее защелок, но обеспечивают гораздо большие функциональные возможности. Внутренние блокировки можно устанавливать на разных уровнях, поэтому можно иметь несколько "разделяемых" блокировок и блокировать с разными уровнями "совместности".

Блокирование вручную. Блокировки, определяемые пользователем До сих пор мы рассматривали в основном блокировки, устанавливаемые сервером Oracle без нашего вмешательства. При изменении таблицы сервер Oracle устанавливает на нее блокировку ТМ, чтобы предотвратить ее удаление (как фактически и применение к ней большинства операторов ЯОД) другими сеансами. В изменяемых блоках оставляют блокировки ТХ — благодаря этому другие сеансы "знают", что с данными рабо Глава тают. Сервер использует блокировки ЯОД для защиты объектов от изменений по ходу их изменения сеансом. Он использует защелки и внутренние блокировки для защиты собственной структуры. Теперь давайте посмотрим, как включиться в процесс блокирования. У нас есть следующие возможности: • блокирование данных вручную с помощью оператора SQL;

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

Блокирование вручную Мы уже описывали несколько случаев, когда может потребоваться блокирование вручную. Основным методом явного блокирования данных вручную является использование оператора SELECT...FOR UPDATE. Мы использовали его в предыдущих примерах для решения проблемы потерянного изменения, когда один сеанс может переписать изменения, сделанные другим сеансом. Мы видели, что этот оператор используется для установки очередности доступа к подчиненным записям, диктуемой бизнес-правилами (вспомните пример с планировщиком ресурсов, приведенный в главе 1). Данные можно также заблокировать вручную с помощью оператора LOCK TABLE. На практике это используется редко в силу особенностей такой блокировки. Этот оператор блокирует таблицу, а не строки в ней. При изменении строк они "блокируются" как обычно. Так что это — не способ экономии ресурсов (как, возможно, в других реляционных СУБД). Оператор LOCK TABLE IN EXCLUSIVE MODE имеет смысл использовать при выполнении большого пакетного изменения, затрагивающего множество строк таблицы, и необходима уверенность, что никто не "заблокирует" это действие. Блокируя таким способом таблицу, можно быть уверенным, что все изменения удастся выполнить без блокирования другими транзакциями. Но приложения с оператором LOCK TABLE встречаются крайне редко.

Создание собственных блокировок Сервер Oracle открывает для разработчиков свои механизмы блокирования для обеспечения очередности доступа с помощью пакета DBMS_LOCK (который подробно описывается в приложении А). Может показаться странным, зачем вообще создавать собственные блокировки. Ответ обычно зависит от того, какое используется приложение. Например, этот пакет может применяться для обеспечения последовательного доступа к ресурсу, внешнему по отношению к серверу Oracle. Пусть используется подпрограмма пакета UTL_FILE, позволяющая записывать информацию в файл в файловой системе сервера. Предположим, разработана общая подпрограмма передачи сообщений, вызываемая приложениями для записи сообщений. Поскольку файл является внешним, сервер Oracle не может координировать доступ нескольких пользователей, пытающихся его менять одновременно. В таких случаях как раз и пригодится пакет DBMS_LOCK. Прежде чем открывать файл, записывать в него и закрывать, можно устанавливать блокировку с именем, соответствующим имени файла, в исключительном режиме, а после закрытия файла вручную снимать эту блокировку. В результате только один пользова Блокирование и одновременный доступ тель в каждый момент времени сможет записывать сообщение в этот файл. Всем остальным придется ждать. Пакет DBMS_LOCK позволяет вручную снять блокировку, когда она уже больше не нужна, или дождаться автоматического ее снятия при фиксации транзакции, или сохранить ее до завершения сеанса.

Что такое управление одновременным доступом?

Средства управления одновременным доступом — это набор функций, предоставляемых сервером баз данных для обеспечения одновременного доступа к данным и их изменения множеством пользователей. Реализация механизма блокирования в базе данных, вероятно, наиболее существенный фактор при определении степени параллелизма, обеспечиваемого приложением (если проще, масштабируемости). Как уже было сказано, есть множество видов блокировок. Это и блокировки транзакций (ТХ), максимально масштабируемые как по производительности, так и по количеству (неважно, одна блокировка в системе или миллион);

и блокировки ТМ и ЯОД (применяемые, по возможности, в минимально ограничивающем режиме). Это и блокировки, используемые сервером Oracle в процессе управления доступом к общим структурам данных, — от очень эффективного и быстрого механизма защелок до более громоздкого, но полнофункционального механизма внутренних блокировок. Но управление одновременным доступом связано не только с блокировками. СУБД может предоставлять и другие средства для обеспечения управляемого, но в значительной степени параллельного доступа к данным. Например, в Oracle имеется средство обеспечения многовариантного доступа (описанное в главе 1). Поскольку сервер Oracle использует многовариантный доступ для обеспечения согласованных по чтению представлений данных, мы получаем весьма приятный побочный эффект: сеанс, читающий данные, никогда не будет заблокирован сеансом, записывающим данные, т.е. запись не блокирует чтения. Это одно из фундаментальных отличий Oracle от остальных СУБД. Запрос на чтение в Oracle никогда не блокируется, он никогда не станет причиной взаимной блокировки с другим сеансом и никогда не даст результат, не существующий в базе данных. Модель многовариантного доступа Oracle для обеспечения согласованности по чтению всегда применяется на уровне оператора (для каждого запроса), но может применяться и на уровне транзакции. В этом разделе я бы хотел продемонстрировать, как многовариантный доступ используется с различными уровнями изолированности транзакции, определяемыми стандартом SQL92.

Уровни изолированности транзакции Стандарт ANSI/ISO SQL92 определяет четыре уровня изолированности транзакции, дающих разные результаты для одного и того же сценария транзакции. То есть выполняются одни и те же действия, одинаковым способом, с теми же данными, но, в зависимости от уровня изолированности транзакции, результат может быть различным. Эти Глава уровни изолированности определяются в терминах трех "явлений", допускаемых или запрещаемых на данном уровне изолированности транзакции: • Грязное чтение (dirty read). Результат настолько же плох, как и название. Допускается чтение незафиксированных, или "грязных", данных. Это случается при открытии обычного файла операционной системы, данные в который записываются другим процессом, и чтении содержимого этого файла. При этом нарушается как целостность данных, так и требования внешнего ключа, а требования уникальности игнорируются. • Неповторяемость при чтении (non-REPEATABLE READ). Это означает, что если строка читается в момент времени T1, а затем перечитывается в момент времени T2, то за этот период она может измениться. Строка может исчезнуть, может быть обновлена и т.д. • Чтение фантомов (phantom read). Это означает, что если выполнить запрос в момент времени T1, а затем выполнить его повторно в момент времени Т2, в базе данных могут появиться дополнительные строки, влияющие на результаты. От неповторяемости при чтении это явление отличается тем, что прочитанные данные не изменились, но критериям запроса стало удовлетворять больше данных, чем прежде. Уровни изолированности SQL92 определяются как допускающие или разрешающие возникновение каждого из описанных выше явлений:

Уровень изолированности READ UNCOMMITTED READ COMMITTED REPEATABLE READ SERIALIZABLE Грязное чтение Разрешено Неповторяемость при чтении Разрешено Разрешено Чтение фантомов Разрешено Разрешено Разрешено Сервер Oracle явно поддерживает уровни изолированности READ COMMITTED и SERIALIZABLE, как они определены стандартом. Однако это еще не все. В стандарте SQL92 была попытка установить уровни изолированности транзакции, обеспечивающие различную степень согласованности для запросов, выполняемых на каждом из уровней. REPEATABLE READ — уровень изолированности, по утверждению создателей, гарантирующий получение согласованных по чтению результатов запроса. При реализации в соответствии с их определением READ COMMITTED не дает согласованных результатов, а уровень изолированности READ UNCOMMITTED используется для не блокирующего чтения. В Oracle уровень изолированности READ COMMITTED позволяет достичь согласованности запросов по чтению. (В других СУБД запросы в транзакции с уровнем READ COMMITTED могут возвращать результаты, никогда не существовавшие в базе данных.) Более того, Oracle поддерживает также то, для чего предназначался уровень изолиро Блокирование и одновременный доступ ванности READ UNCOMMITTED. Грязное чтение применяется в качестве не блокирующего, т.е. запросы не блокируются при изменении данных и сами не блокируют изменения читаемых данных. Однако серверу Oracle для этого не нужно грязное чтение (он его и не поддерживает). Грязное чтение — это реализация не блокирующего чтения, которое вынуждены использовать другие СУБД. Помимо четырех уровней изолированности транзакции, определенных в стандарте SQL92, Oracle обеспечивает еще один уровень — транзакции только для чтения. Транзакция только для чтения эквивалентна при чтении уровню изолированности REPEATABLE READ или SERIALIZABLE в SQL92. Такая транзакция видит только изменения, зафиксированные на момент ее начала, но в этом режиме не разрешена вставка, изменение и удаление данных (другие сеансы могут изменять данные, но транзакция только для чтения — нет). Используя этот режим, можно достичь уровней REPEATABLE READ и SERIALIZABLE READ без чтения фантомов. Теперь давайте рассмотрим, как многовариантный доступ и согласованность по чтению соотносятся с перечисленными выше схемами изолированности и как другие СУБД, не поддерживающие многовариантного доступа, достигают тех же результатов. Это будет инструктаж для пользователей других СУБД и тех, кто верит, будто знает, что должны обеспечивать уровни изолированности. Также будет интересно посмотреть, как стандарт, SQL92, который, казалось бы, должен сглаживать различия между СУБД, фактически их допускает. Этот очень детальный стандарт может быть реализован множеством способов.

Уровень изолированности READ UNCOMMITTED Уровень изолированности READ UNCOMMITTED разрешает грязное чтение. Сервер Oracle не использует грязного чтения, и не допускает его. Основное назначение уровня изолированности READ UNCOMMITTED — стандартное определение не блокирующего чтения. Как уже было показано, Oracle пo умолчанию обеспечивает не блокирующее чтение. Надо создать специальные условия, чтобы оператор SELECT блокировал что-либо в базе данных (есть частный случай сомнительной распределенной транзакции, который рассматривается в главе 4). Каждый отдельный оператор, будь-то SELECT, INSERT, UPDATE или DELETE, выполняется как согласованный по чтению. Реализуемый сервером Oracle способ достижения согласованности по чтению был продемонстрирован в главе 1 на примере банковских счетов. Здесь мы снова вернемся к этому примеру, чтобы более детально описать, что происходит в СУБД Oracle при многовариантном доступе, а что — в других СУБД. Напомню, что в блоке данных предполагалось наличие одной строки таблицы. Начнем с той же простой таблицы и запроса:

create table accounts ( account_number number primary key, account_balance number );

s e l e c t sum(account balance) from accounts;

Глава Перед началом запроса имеются следующие данные: Строка 1 2... 342023... 987 Номер счета 123 456... 100,00 $ Баланс счета 500,00 $ 240,25 $ Теперь оператор SELECT начинает выполняться и читать строку 1, строку 2 и так далее. В какой-то момент выполнения запроса некая транзакция переводит 400,00 $ со счета 123 на счет 987. Эта транзакция делает два изменения, но не фиксирует их. Таблица теперь выглядит следующим образом: Строка 1 2 342,023 Номер счета 123 456 987 Баланс счета (500,00 $) изменился и теперь равен 100,00 $ $240,25 (100,00 $) изменился и теперь равен 500,00 $ X ЗАБЛОКИРОВАНА X Итак, обе измененные строки заблокированы для изменения. В этом отношении все СУБД работают примерно одинаково. Различия появятся, когда запрос SELECT доберется до заблокированных данных. Добравшись до заблокированного блока, выполняемый запрос "поймет", что данные в нем изменились после начала выполнения. Чтобы дать согласованный (правильный) ответ, сервер Oracle в этот момент восстановит блок с заблокированными данными в том виде, как он был на момент начала выполнения запроса. Таким образом, сервер Oracle обходит блокировку — он читает, восстанавливая соответствующие данные из сегмента отката. Согласованный и правильный ответ получается без ожидания фиксации транзакции. Однако СУБД, допускающая грязное чтение, просто вернет значение баланса счета 987 на момент его чтения, в данном случае 500 $. Запрос учтет перевод суммы 400 $ дважды и выдаст сумму, которая никогда не фигурировала в таблице счетов. В многопользовательской базе данных грязное чтение может быть опасным, и лично я никогда не видел от него пользы. В результате такого чтения не только выдается неверный результат, но могут выдаваться данные, никогда не существовавшие в базе данных. Пусть вместо перевода денег с другого счета транзакция добавила бы 400 $ на счет 987. Грязное чтение добавило бы 400 $ и выдало "правильный" ответ, не так ли? А если не зафиксированная транзакция будет отменена? Нам только что предъявили 400 $, которых в базе данных фактически никогда не было. Дело в том, что грязное чтение — это не возможность, это просто лишняя ответственность. В Oracle брать ее на себя нет необходимости. Мы получаем все преимущества грязного чтения (отсутствие блокирования) без каких-либо некорректных результатов.

Блокирование и одновременный доступ Уровень изолированности READ COMMITTED Уровень изолированности READ COMMITTED требует, чтобы транзакция читала только данные, зафиксированные до ее начала. Никаких грязных чтений. Неповторямость при чтении допускается (повторное чтение той же строки в транзакции может дать другой результат), как и чтение фантомов (по запросу могут возвращаться вновь вставленные строки, невидимые ранее для транзакции). READ COMMITTED, вероятно, — наиболее часто и повсеместно используемый в приложениях уровень изолированности транзакции. Другие уровни изолированности используются редко. Уровень READ COMMITTED не настолько безобиден, как можно судить по названию. Если взглянуть на представленную ранее таблицу, все кажется вполне очевидным. Естественно, с учетом указанных выше правил, запрос к базе данных, выполненный в транзакции с уровнем изолированности READ COMMITTED, должен выполняться одинаково, не так ли? Нет. Если запрос возвращает несколько строк, практически в любой СУБД уровень изолированности READ COMMITTED может стать причиной появления некорректных данных в той же мере, что и грязное чтение. В Oracle, благодаря использованию многовариантного доступа и согласованных по чтению запросов, результат запроса к таблице счетов при уровне изолированности READ COMMITTED будет таким же, как и в примере с READ UNCOMMITTED. Сервер Oracle восстанавливает измененные данные в том виде, какой они имели на момент начала выполнения запроса, возвращая ответ, соответствующий состоянию базы данных на тот же момент. Теперь давайте рассмотрим, как представленный выше пример мог бы выполняться в режиме READ COMMITTED в других СУБД. Результат удивит вас. Повторим тот же пример: • Мы находимся в середине таблицы, прочитав и просуммировав к этому моменту N строк. • Другая транзакция перевела 400 $ со счета 123 на счет 987. • Эта транзакция еще не зафиксирована, поэтому строки 123 и 987 заблокированы. Мы знаем, как поведет себя СУБД Oracle, добравшись до счета 987: она прочитает измененные данные из сегмента отката, подсчитает баланс (значение окажется равным 100,00 $) и завершит запрос. Давайте рассмотрим, как другая СУБД, работающая в том же стандартном режиме READ COMMITTED, будет выдавать ответ. Время Запрос Т1 Т2 ТЗ Читает строку 1, sum = 500 $. Читает строку 2, sum == 740,25 $. Изменяет строку 1, устанавливает исключительную блокировку на блок 1, предотвращающую другие изменения и чтение. В строке 1 теперь значение 100 $. Читает строку N, sum =.... Транзакция по переводу со счета на счет Т Т Глава Время Запрос Транзакция по переводу со счета на счет Изменяет строку 342023, устанавливает на нее исключительную блокировку. В строке теперь значение 500 $. Читает строку 342023, обнаруживает, что она была изменена. Сеанс блокируется в ожидании доступа к соответствующей строке. Выполнение запроса приостанавливается. Транзакция фиксируется. Читает строку 342023, находит в ней баланс 500 $ и выдает окончательный результат.

Тб Т7 Т Обратите внимание, что в другой СУБД запрос будет заблокирован, когда доберется до счета 987. Нашему сеансу придется ждать освобождения соответствующей строки, пока не зафиксируется транзакция, удерживающая исключительную блокировку. Это одна из причин появления у многих разработчиков плохой привычки фиксировать результаты в середине транзакции. В большинстве других СУБД изменения мешают чтению. Но самое печальное в этом то, что пользователь вынужден ожидать неверного результата. Как и в случае грязного чтения, получен результат, никогда не существовавший в базе данных, но в этом случае пришлось еще и ожидать. Важный урок здесь в том, что разные СУБД, выполняющие запрос с одним, предположительно безопасным, уровнем изолированности транзакции, возвращают существенно отличающиеся результаты в абсолютно одинаковых обстоятельствах. Важно понимать, что в Oracle не блокирующее чтение дается не ценой некорректности результатов. Оказывается, иногда можно, как говорится, и рыбку съесть, и на елку влезть.

Уровень изолированности REPEATABLE READ Целью включения REPEATABLE READ в стандарт SQL92 было обеспечение уровня изолированности транзакций, дающего согласованные, корректные результаты и предотвращающего потерю изменений. На двух примерах будет показано, как это достигается в Oracle и что происходит в других системах.

Получение согласованного ответа При установке уровня изолированности REPEATABLE READ результаты запроса должны быть согласованными на определенный момент времени. Обычно СУБД (но не Oracle) достигают уровня REPEATABLE READ за счет установки строчных разделяемых блокировок чтения. Разделяемая блокировка чтения предотвращает изменение прочитанных данных другими сеансами. Это, несомненно, снижает параллелизм. В Oracle для получения согласованных по чтению результатов выбрана модель многовариантного доступа, обеспечивающая больший параллелизм.

Блокирование и одновременный доступ При использовании многовариантного доступа в Oracle получается ответ, согласованный на момент начала выполнения запроса. В других СУБД при использовании разделяемых блокировок чтения получается результат, согласованный на момент завершения запроса — на момент, когда вообще можно получить результат (подробнее об этом — ниже). В системе, использующей для обеспечения уровня изолированности REPEATABLE READ разделяемые блокировки чтения, строки будут блокироваться по мере их обработки запросом. Поэтому, возвращаясь к рассматривавшемуся выше примеру, по мере чтения запросом таблицы счетов, он будет устанавливать разделяемые блокировки чтения на каждой строке:

Время Запрос Транзакция по переводу со счета на счет Т Читает строку 1, sum = 500 $. На блоке 1 установлена разделяемая блокировка. Читает строку 2, sum = 740,25 $. На блоке 2 установлена разделяемая блокировка. Пытается изменить строку 1, но эта попытка блокируется. Транзакция приостанавливается, пока не сможет установить исключительную блокировку. Читает строку N, sum =.... Читает строку 342023, находит баланс счета, 100 $, и дает окончательный результат. Фиксирует транзакцию. Изменяет строку 1, устанавливая на соответствующий блок исключительную блокировку. Теперь баланс в этой строке имеет значение 100 $. Изменят строку 342023, устанавливая на соответствующий блок исключительную блокировку. Теперь баланс в этой строке имеет значение 500 $. Транзакция фиксируется.

Т ТЗ Т4 Т Т6 Т Т Эта таблица показывает, что корректный результат получен благодаря физическому упорядочению двух транзакций. Вот один из побочных эффектов использования разделяемых блокировок чтения для получения согласованных результатов: сеансы считывания данных блокируют сеансы записи данных. Кроме того, в этих системах сеансы записи данных блокируют сеансы их считывания. Итак, понятно, как разделяемые блокировки чтения снижают параллелизм, но они также могут приводить к возникновению спорадических ошибок. В следующем примере мы начнем с исходной таблицы счетов, но в этот раз целью будет перевод 50,00 $ со счета 987 на счет 123.

Время Т Глава Запрос Читает строку 1, sum = 500 $. На строке 1 установлена разделяемая блокировка. Читает строку 2, sum = 740,25 $. На строке 2 установлена разделяемая блокировка, Транзакция по переводу со счета на счет Т ТЗ Изменяет строку 342023, устанавливает исключительную блокировку на строку 342023, предотвращающую другие изменения и установку разделяемых блокировок. В этой строке теперь содержится значение 50 $. Читает строку N, sum =.... Пытается изменить строку 1, но она заблокирована. Транзакция приостанав ливается до тех пор, пока не появится возможность установить исключительную блокировку. Пытается прочитать строку 342023, но не может, поскольку на нее уже установлена исключительная блокировка.

Т4 Т Т Только что мы получили классическую ситуацию взаимной блокировки. Наш запрос удерживает ресурс, необходимый транзакции, изменяющей данные, и наоборот. Запрос и изменяющая транзакция взаимно заблокировали друг друга. Один из сеансов будет выбран в качестве жертвы, и его транзакция будет прекращена. Затрачено немало времени и ресурсов и все впустую: произошел откат. Это — второй побочный эффект разделяемых блокировок чтения: сеансы чтения и сеансы записи данных могут взаимно блокировать друг друга, и часто так и происходит. Как уже было показано, в Oracle обеспечивается согласованность по чтению на уровне операторов без блокирования сеансов записи сеансами чтения или взаимного блокирования. Сервер Oracle никогда не использует разделяемые блокировки чтения. Разработчики Oracle выбрали более сложную в реализации, но обеспечивающую принципиально более высокую степень параллелизма схему многовариантного доступа.

Предотвращение потери изменений Чаще всего уровень изолированности транзакции REPEATABLE READ используется для предотвращения потери изменений. При установке уровня REPEATABLE READ это не происходит. По определению (повторяющиеся чтения) повторное чтение строки в той же транзакции даст точно такой же результат. В других СУБД, кроме Oracle, REPEATABLE READ может реализовываться с помощью SELECT FOR UPDATE и разделяемых блокировок чтения. Если два пользователя выбрали одну и ту же строку для изменения, они оба устанавливают на нее разделяе Блокирование и одновременный доступ мую блокировку чтения. Когда первый пользователь попытается изменить строку, эта попытка будет заблокирована. В случае попытки второго пользователя выполнить изменение возникнет взаимная блокировка. Это не идеально, но предотвращает потерю изменений. Если в Oracle необходим уровень изолированности REPEATABLE READ, но не хочется физически упорядочивать доступ к таблице с помощью операторов SELECT FOR UPDATE NOWAIT (как было продемонстрировано в начале главы), придется устанавливать уровень изолированности SERIALIZABLE. SERIALIZABLE покрывает все более либеральные уровни изолированности, поэтому, если можно выполнять доступ уровня SERIALIZABLE, то возможен доступ и уровня REPEATABLE READ. В Oracle транзакция с уровнем изолированности SERIALIZABLE реализуется так, что согласованность по чтению, обычно получаемая на уровне оператора, распространяется на всю транзакцию. То есть результаты каждого выполняемого в транзакции запроса соответствуют состоянию базы данных на момент начала транзакции. Если выполнить в этом режиме:

Select * from T;

Begin dbms_lock.sleep(60*60*24);

end;

Select * from T;

результаты, возвращаемые из таблицы Т, будут одинаковыми и через 24 часа (или мы получим сообщение об ошибке ORA-1555, snapshot too old). Уровень изолированности гарантирует, что эти два запроса всегда будут возвращать одинаковые результаты. В Oracle это достигается тем же способом, что и согласованность по чтению одного запроса. Сервер использует сегменты отката для воссоздания данных в том виде, как они были на момент начала транзакции, а не на момент начала выполнения оператора. Если же мы пытаемся изменить данные в транзакции с уровнем изолированности SERIALIZABLE и обнаруживаем, что данные изменились после ее начала, мы получаем сообщение об ошибке, информирующее о том, что невозможно обеспечить последовательность доступа. Вскоре мы рассмотрим это подробнее. Понятно, что этот подход не оптимален в случае рассмотренного в начале главы приложения для отдела кадров. В этом приложении типична ситуация, когда оба пользователя запрашивают данные, а затем оба изменяют их на экране. Первый пользователь пытается сохранить изменения, и у него это успешно получается. Второй же пользователь при попытке сохранить изменения получит сообщение об ошибке. Он зря потратил время. Ему придется перезапустить транзакцию, получить произошедшие за это время изменения и сделать все сначала. Потеря изменений предотвращается, но ценой дополнительных неудобств для пользователя. Однако если ситуация требует использования уровня изолированности транзакции REPEATABLE READ и не предполагается одновременное изменение несколькими транзакциями одних и тех же строк, то использование уровня изолированности SERIALIZABLE вполне допустимо.

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

база данных "замораживается" на момент начала транзакции. Для транзакции база данных выглядит согласованной, в виде своего рода моментального снимка. Побочные эффекты (изменения) от других транзакций для такой транзакции невидимы, независимо от того, как долго она выполняется. Уровень изолированности SERIALIZABLE не означает, что все пользовательские транзакции дают такой же результат, как и при последовательном выполнении. Он не предполагает наличия физического порядка выполнения транзакций, дающего такой же результат. Это последнее утверждение часто понимают неправильно, и небольшой пример сейчас все прояснит. В следующей таблице представлены два сеанса, выполняющие определенную последовательность действий. Таблицы базы данных А и В первоначально пусты и созданы следующими операторами:

tkyte@TKYTE816> create table a (x int) ;

Table created. tkyte@TKYTE816> create table b (x int) ;

Table created.

Теперь происходят следующие события:

Время Сеанс 1 выполняет Alter session set isolation_level=serializable;

Сеанс 2 выполняет 0:00 0:01 0:02 0:03 0:04 0: Alter session set isolation_level=serializable;

Insert into a select count(*) from b;

Insert into b select count(*) from a;

Commit;

Commit;

Теперь в каждой из таблиц имеется по строке со значением ноль. Если бы выполнялось "последовательное" упорядочение транзакций, мы не могли бы получить нулевые значения в обеих таблицах. Если бы сеанс 1 опережал сеанс 2, то в таблице В было бы значение 1. Если бы сеанс 2 выполнялся прежде сеанса 1, то в таблице А было бы значение I. Однако при выполнении приведенной выше последовательности действий в обеих таблицах будут нулевые значения, т.е. транзакции выполняются так, будто других транзакций в базе данных в этот момент нет. Неважно, сколько раз сеанс 1 запрашивает таблицу В, — будет выдано количество строк, зафиксированных в базе данных на момент времени 0:00. Аналогично, независимо от количества запросов таблицы А, в сеансе 2 будет получено значение на момент времени 0:01. В Oracle последовательность достигается путем распространения согласованности по чтению, получаемой на уровне оператора, на уровень транзакции. Результаты не согласуются на момент начала выполнения оператора — они предопределены моментом начала транзакции. Весьма глубокая мысль: базе данных известен ответ на любой возможный ваш вопрос прежде, чем вы его зададите.

Блокирование и одновременный доступ Этот уровень изолированности не дается даром: за него нужно платить сообщением об ошибке:

ERROR at line 1: ORA-08177: can't serialize access for this transaction Это сообщение будет получено при любой попытке обновления строки, измененной после начала транзакции. Сервер Oracle придерживается при обеспечении последовательности оптимистического подхода;

он предполагает, что данные, которые потребуется изменять вашей транзакции, не будут изменены другой транзакцией. Обычно именно так и происходит, и подобное предположение вполне оправдано, особенно в системах оперативной обработки транзакций (ООТ). Если в ходе транзакции ваши данные не изменяются другим сеансом, этот уровень изолированности, обычно снижающий степень параллелизма в других системах, обеспечит ту же степень параллелизма, что и при отсутствии транзакций с уровнем изолированности SERIALIZABLE. Вопрос лишь в том, не получите ли вы сообщения об ошибке ORA-08177, если предположение не оправдается. Однако поразмыслив, можно найти такой риск оправданным. Если уж используется транзакция с уровнем изолированности SERIALIZABLE, нет смысла ожидать изменения той же информации другими транзакциями. Если это возможно, используйте оператор SELECT... FOR UPDATE, как было показано выше, который обеспечит требуемую последовательность доступа. Итак, если • высока вероятность того, что данные не изменяет другой сеанс;

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

• транзакции будут непродолжительными (чтобы первое условие стало более реальным);

то использование уровня изолированности SERIALIZABLE дает хороший эффект. Корпорация Oracle считает этот метод настолько масштабируемым, что выполняет на таком уровне изолированности все тесты TPC-C (стандартный набор тестов производительности систем ООТ;

подробнее см. на сайте http://www.tpc.org). Во многих других реализациях это достигается путем использования разделяемых блокировок чтения со всеми соответствующими взаимными блокировками и ожиданием. В Oracle блокирования нет, но, если другие сеансы изменяют данные, необходимые вашему сеансу, будет получено сообщение об ошибке ORA-08177. Однако это сообщение об ошибке генерируется гораздо реже, чем происходят взаимные блокировки и ожидания снятия блокировок в других системах.

Транзакции только для чтения Транзакции только для чтения очень похожи на транзакции с уровнем изолированности SERIALIZABLE. Единственное отличие в том, что они не разрешают изменять данные, поэтому не подвержены ошибке ORA-08177. Транзакции только для чтения предназначены для создания отчетов, когда данные отчета должны быть согласованы по отношению к определенному моменту времени. В других системах придется использовать уровень изолированности REPEATABLE READ, получая нежелательные последствия разделяемых блокировок чтения. В Oracle используется транзакция только для Глава чтения. В этом режиме результат, получаемый в отчете на основе данных, собранных 50 операторами SELECT, будет согласован по отношению к одному моменту — началу транзакции. Это можно сделать, не блокируя ни одного компонента данных, где бы то ни было. Достигается это с помощью того же многовариантного доступа, что и для отдельных операторов. Данные восстанавливаются при необходимости из сегментов отката и представляются в том виде, какой они имели до начала создания отчета. Однако использование транзакций только для чтения также не лишено проблем. Тогда как сообщение об ошибке ORA-08177 вполне вероятно для транзакций с уровнем изолированности SERIALIZABLE, при использовании транзакций только для чтения можно ожидать сообщений об ошибке ORA-1555 snapshot too old. Они будут выдаваться в системе, где другие сеансы активно изменяют считываемую нами информацию. Их изменения (данные отмены — undo) записываются в сегменты отката. Но сегменты отката используются циклически, аналогично журналам повторного выполнения. Чем более продолжительное время создается отчет, тем выше вероятность, что часть данных отмены, необходимых для восстановления данных, перестанет быть доступной. Сегмент отката будет использоваться повторно, и необходимая нам его часть окажется использованной другой транзакцией. В этот момент сеанс и получит сообщение об ошибке ORA-1555, и придется начинать все с начала. Единственное решение этой болезненной проблемы — сконфигурировать в системе сегменты отката адекватного размера. Постоянно приходится сталкиваться с ситуацией, когда, пытаясь сэкономить несколько мегабайт дискового пространства, создают сегменты отката минимально возможного размера (зачем "тратить" место на то, что мне фактически не нужно?). Проблема в том, что сегменты отката — ключевой компонент, обеспечивающий функционирование базы данных, и если их размер окажется меньше, чем нужно, — ошибка ORA-1555 гарантирована. За 12 лет использования Oracle 6, 7 и 8 я никогда не сталкивался с ошибкой ORA-1555 за пределами тестовой системы или среды разработки. При возникновении этих ошибок сразу становится понятно, что сегменты отката имеют недостаточный размер, но это легко исправляется. Мы еще вернемся к этой проблеме в главе 5.

Резюме В этой главе мы рассмотрели большой объем материала, иногда трудного для понимания. Тогда как блокирование — тема весьма простая, некоторые его побочные эффекты понять сложнее. Однако знание всего этого жизненно важно. Например, если не знать о блокировке таблицы, используемой сервером Oracle для обеспечения требования внешнего ключа, когда он не проиндексирован, приложения будут иметь низкую производительность. Если не представлять себе, как определить по словарю данных, кто кого блокирует, трудно разобраться в причине происходящего. Просто покажется, что временами база данных "зависает". Если бы я получал по доллару всякий раз, когда устранял "неразрешимую" проблему зависания с помощью запроса, выявляющего неиндексированные внешние ключи и предлагающего соответствующий индекс, я бы уже был очень богатым человеком.

Блокирование и одновременный доступ Мы выяснили смысл уровней изолированности транзакции, устанавливаемых стандартом SQL92, и то, как эти уровни реализуются в Oracle и в других СУБД. Мы убедились, что в тех случаях, когда для обеспечения согласованности данных используются блокировки чтения, приходится идти на компромисс между параллелизмом и согласованностью данных. Для получения высокой степени параллелизма при доступе к данным необходимо снизить требования к согласованности результатов. Для получения согласованных, правильных результатов необходимо снижать параллелизм. Мы увидели, что в случае сервера Oracle, благодаря многовариантному доступу, такой проблемы не существует. Приведенная ниже небольшая таблица наглядно демонстрирует, чего можно ожидать от СУБД, использующей блокирование при чтении, по сравнению с многовариантным доступом в Oracle:

READ UNCOMMITTED READ COMMITTED REPEATABLE READ SERIALIZABLE READ COMMITTED SERIALIZABLE Блокирование при чтении (Другие СУБД) Нет Да Да Да Нет Нет Да Да Нет Нет Нет Нет Да Да Нет Нет Да Да Нет Нет Нет Нет Да Да Нет Нет Нет* Нет Да Да Да Да Нет Нет Многовариантный Нет доступ (Oracle) Нет * При использовании select for update nowait Все, что касается средств управления одновременным доступом и их реализации в СУБД, необходимо глубоко усвоить. Я пел дифирамбы многовариантному доступу и согласованности по чтению, но, как и все под небесами, эти средства неоднозначны. Если не понимать, что они собой представляют и как работают, ошибки при разработке приложений неизбежны. Рассмотрим пример с планированием ресурсов из главы 1. В СУБД без многовариантного доступа и связанных с ним не блокируемых чтений подход, изначально заложенный в программу, может себя вполне оправдать. Однако этот подход неприемлем в СУБД Oracle — будет нарушаться целостность данных. Если не знать возможных последствий, можно написать программу, повреждающую данные. Вот так — все просто.

Транзакции Транзакции — одно из свойств, отличающих базу данных от файловой системы. В файловой системе, если сбой ОС происходит во время записи файла, он, скорее всего, окажется поврежденным. Существуют различные "журнализируемые" файловые системы, которые позволяют восстановить файл на определенный момент времени. Однако, если необходима синхронизация двух файлов, это не поможет: когда при изменении одного файла сбой происходит до того как завершится изменение второго, файлы окажутся рассинхронизированными. Основное назначение транзакций в базе данных — переводить ее из одного согласованного состояния в другое. При фиксации изменений в базе данных гарантируется сохранение либо всех изменений, либо ни одного. Более того, выполняются все правила и проверки, обеспечивающие целостность данных. Транзакции базы данных обладают свойствами, сокращенно называемыми ACID (Atomicity, Consistency, Isolation, Durability). Вот что означают эти свойства: • Неделимость (Atomicity). Транзакция либо выполняется полностью, либо не выполняется. • Согласованность (Consistency). Транзакция переводит базу данных из одного согласованного состояния в другое.

• Изолированность (Isolation). Результаты транзакции становятся доступны для других транзакций только после ее фиксации. • Продолжительность (Durability). После фиксации транзакции изменения становятся постоянными.

Глава Транзакции в Oracle обладают всеми перечисленными выше характеристиками. В этой главе мы опишем влияние неделимости на выполнение операторов в СУБД Oracle. Будут рассмотрены операторы управления транзакцией, такие как COMMIT, SAVEPOINT и ROLLBACK, и то, как в транзакции обеспечивается выполнение требований целостности. Мы также постараемся выяснить, почему приобретаются плохие привычки в работе с транзакциями при разработке приложений для других СУБД. Разберемся с распределенными транзакциями и двухэтапной фиксацией. Наконец, рассмотрим реальные проблемы, возникающие при использовании и журнализации транзакций, а также выясним, какую роль могут играть сегменты отката.

Операторы управления транзакцией В СУБД Oracle нет оператора "начать транзакцию". Транзакция неявно начинается с первого же оператора, изменяющего данные (установившего блокировку ТХ). Операторы COMMIT или ROLLBACK явно завершают транзакции. Всегда завершайте транзакции явно с помощью оператора COMMIT или ROLLBACK, иначе решение о том, фиксировать или откатывать, автоматически примет используемое инструментальное средство или среда. При обычном выходе из сеанса SQL*Plus без фиксации или отката эта утилита предполагает, что нужна фиксация, и автоматически ее выполняет. При завершении же программы на языке Pro*C по умолчанию выполняется откат. Транзакции в Oracle неделимы: либо фиксируется (делается постоянным) результат выполнения каждого из операторов, составляющих транзакцию, либо результаты выполнения всех операторов откатываются. Эта защита распространяется и на отдельные операторы. Оператор либо завершается полностью успешно, либо полностью откатывается. Обратите внимание: я написал, что оператор откатывается. Сбой в одном операторе не вызывает автоматического отката ранее выполненных операторов. Их результаты сохраняются и должны быть зафиксированы или отменены пользователем. Прежде чем разбираться детально, что означает свойство "неделимости" для оператора и транзакции, рассмотрим операторы управления транзакциями. • COMMIT. В простейшем случае достаточно ввести просто COMMIT. Можно быть более многословным и выполнить COMMIT WORK, но обе формы эквивалентны. Оператор COMMIT завершает транзакцию и делает любые выполненные в ней изменения постоянными (продолжительными). В распределенных транзакциях используются расширения оператора COMMIT. Эти расширения позволяют пометить оператор COMMIT (точнее, пометить транзакцию), задав для него комментарий, а также принудительно зафиксировать сомнительную распределенную транзакцию. • ROLLBACK. В простейшем случае выполняется просто оператор ROLLBACK. Можно также использовать форму ROLLBACK WORK, но обе формы эквивалентны. Простой оператор отката завершает транзакцию и отменяет все выполненные в ней и незафиксированные изменения. Для этого он читает информацию из сегментов отката и восстанавливает блоки данных в состояние, в котором они находились до начала транзакции.

Транзакции • SAVEPOINT. Оператор SAVEPOINT позволяет создать в транзакции "метку", или точку сохранения. В одной транзакции можно выполнять оператор SAVEPOINT несколько раз, устанавливая несколько точек сохранения. • ROLLBACK TO <точка сохранения>. Этот оператор используется совместно с представленным выше оператором SAVEPOINT. Транзакцию можно откатить до указанной точки сохранения, не отменяя все сделанные до нее изменения. Таким образом, можно выполнить два оператора UPDATE, затем — оператор SAVEPOINT, а после него — два оператора DELETE. При возникновении ошибки или исключительной ситуации в ходе выполнения операторов DELETE транзакция будет откатываться до указанной оператором SAVEPOINT точки сохранения;

при этом будут отменяться операторы DELETE, но не операторы UPDATE. • SET TRANSACTION. Этот оператор позволяет устанавливать атрибуты транзакции, такие как уровень изолированности и то, будет ли она использоваться только для чтения данных или для чтения и записи. Этот оператор также позволяет привязать транзакцию к определенному сегменту отката. Вот и все операторы управления транзакциями. Чаще всего используются операторы COMMIT и ROLLBACK. Оператор SAVEPOINT имеет несколько специфическое назначение. Сервер Oracle часто использует его по ходу работы, но определенную пользу от него можно получить и в приложениях. Теперь можно приступить к изучению последствий неделимости оператора и транзакции. Рассмотрим следующий оператор:

Insert into t values (1) ;

Кажется вполне очевидным, что строка вставлена не будет, если из-за нарушения требования уникальности этот оператор не выполнится. Рассмотрим, однако, следующий пример, где при вставке или удалении строки в таблице Т срабатывает триггер, изменяющий значение столбца cnt в таблице Т2:

tkyte@TKYTE816> create table t2 Table created. (cnt int) ;

tkyte@TKYTE816> insert into t2 values 1 row created. tkyte@TKYTE816> create table t Table created.

(0);

(x int check (x>0)) ;

tkyte@TKYTE816> create trigger t_trigger 2 before insert or delete on t for each row 3 begin 4 if (inserting) then 5 update t2 set cnt = cnt +1;

6 else 7 update t2 set cnt = cnt - 1 ;

8 end if;

9 dbms_output.put_line('I fired and updated ' || sql%rowcount || ' rows');

Глава 10 end;

11 / Trigger created.

В этой ситуации предполагаемые последствия менее очевидны. Если ошибка происходит после срабатывания триггера, должны ли остаться в силе изменения, сделанные триггером? То есть, если триггер сработал, изменив таблицу Т2, а строка не вставлена в таблицу Т, каким должен быть результат? Естественно, не хотелось бы, чтобы значение в столбце cnt таблицы Т2 было увеличено, если строка в таблицу Т не вставлена. К счастью, в Oracle переданный клиентом оператор (INSERT INTO T в данном случае) либо выполняется успешно полностью, либо не выполняется вообще. Это — неделимый оператор. В этом можно убедиться следующим образом:

tkyte@TKYTE816> set serveroutput on tkyte@TKYTE816> insert into t values (1) ;

I fired and updated 1 rows 1 row created. tkyte@TKYTE816> insert into t values (-1) ;

insert into t values (-1) * ERROR at line 1: ORA-02290: check constraint (TKYTE.SYS_C001570) violated tkyte@TKYTE816> exec null /* это необходимо для получения /* результатов dbms_output I fired and updated 1 rows PL/SQL procedure successfully completed. tkyte@TKYTE816> select * from t2;

CNT 1 */ */ Мы успешно вставили одну строку в таблицу Т, получив надлежащее сообщение: I fired and updated 1 rows. Следующий оператор INSERT нарушает ограничение целостности таблицы Т. Мне пришлось ввести exec NULL и выполнить тем самым пустой оператор, чтобы утилита SQL*Plus показала информацию, выданную с помощью пакета DBMS_OUTPUT, (поскольку SQL*Plus не выдает содержимое буфера DBMS_OUTPUT после выполнения оператора SELECT), но и в этот раз было получено сообщение, что триггер сработал и изменил одну строку. Можно было бы предположить, что в таблице Т2 теперь будет значение 2, но мы видим там значение I. Сервер Oracle обеспечил неделимость переданного оператора вставки. Он достигает этого, неявно размещая операторы SAVEPOINT вокруг каждого из переданных операторов. Представленные выше операторы вставки на самом деле обрабатываются так:

Транзакции Savepoint statement1;

Insert into t values (1) ;

If error then rollback to statement1;

Savepoint statement2;

Insert into t values (-1) ;

If error then rollback to statement2;

Для программистов, привыкших работать с СУБД Sybase или SQL Server, это поначалу кажется странным. В этих СУБД все происходит в точности наоборот. В этих системах триггеры выполняются независимо от вызвавшего их срабатывание оператора. Если в них происходит ошибка, триггеры должны явно откатывать сделанные ими изменения, а затем возбуждать другую исключительную ситуацию для отката оператора, вызвавшего срабатывание. В противном случае выполненные триггером изменения могут остаться в силе, даже если вызвавший срабатывание оператор завершился неудачно. В Oracle неделимость операторов распространяется на любую необходимую глубину. В рассмотренном выше примере оператор INSERT INTO T вызвал срабатывание триггера, изменившего другую таблицу, а для этой таблицы есть триггер, удаляющий строки из еще одной таблицы, и так далее, но в любом случае либо все изменения выполняются успешно, либо ни одно. Для этого не надо применять специальные приемы кодирования — все работает именно так. Интересно отметить, что сервер Oracle считает оператором анонимный блок PL/SQL. Рассмотрим следующую хранимую процедуру:

tkyte@TKYTE816> create or replace procedure p 2 as 3 begin 4 insert into t values (1) ;

5 insert into t values (-1) ;

6 end;

7/ Procedure created. tkyte@TKYTE816> select * from t;

no rows selected tkyte@TKYTE816> select * from t2;

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

tkyte@TKYTE816> begin 2 Р;

3 end;

4/ I fired and updated 1 rows I fired and updated 1 rows begin Глава ERROR at line 1: ORA-02290: check constraint (TKYTE.SYS_C001570) violated ORA-06512: at "TKYTE.P", line 5 ORA-06512: at line 2 tkyte@TKYTE816> select * from t;

no rows selected tkyte@TKYTE816> select * from t2;

CNT Как видите, сервер Oracle счел вызов процедуры неделимым оператором. Клиент послал блок кода (BEGIN P;

END;

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

tkyte@TKYTE816> begin 2 Р;

3 exception 4 when others then null;

5 end;

6/ I fired and updated 1 rows I fired and updated 1 rows PL/SQL procedure successfully completed. tkyte@TKYTE816> select * from t;

X 1 tkyte@TKYTE816> select * from t2;

CNT Мы выполнили блок кода, в котором игнорируются любые ошибки, и результат получился принципиально другим. Тогда как первый вызов процедуры Р не вызвал никаких изменений, в данном случае первый оператор INSERT выполняется успешно, и столбец cnt в таблице Т2 увеличивается на 1. Сервер Oracle считает "оператором" переданный клиентом блок. Этот оператор завершается успешно, самостоятельно перехватывая и игнорируя ошибку, так что фрагмент "If error then rollback..." не срабатывает, и сервер Oracle не выполняет откат до точки сохранения после его выполнения. Вот по Транзакции чему результаты работы процедуры частично Р сохраняются. Причина этого частичного сохранения прежде всего в том, что внутри Р соблюдается неделимость операторов: все операторы в процедуре Р неделимы. Процедура Р работает как клиент Oracle при посылке двух операторов INSERT. Каждый оператор INSERT либо успешно выполняется полностью, либо завершается неудачно. Это подтверждается тем фактом, что триггер таблицы Т срабатывает дважды и два раза изменяет таблицу Т2, хотя счетчик в Т2 отражает только одно изменение. Вокруг второго оператора INSERT, выполненного в процедуре Р, были установлены неявные точки сохранения. Различие между двумя приведенными блоками кода невелико, но его надо учитывать при разработке приложений. Добавление обработчика исключительных ситуаций в блок кода PL/SQL может радикально изменить его работу. Ниже приведен более корректный способ реализации того же подхода, расширяющий неделимость на уровне оператора до уровня всего блока PL/SQL:

tkyte@TKYTE816> begin 2 savepoint sp;

P;

4 exception 5 when others then 6 rollback to sp;

7 end;

8/ I fired and updated 1 rows I fired and updated 1 rows PL/SQL procedure successfully completed. tkyte@TKYTE816> tkyte@TKYTE816> select * from t;

no rows selected tkyte@TKYTE816> select * from t2;

CNT Имитируя автоматически выполняемые сервером Oracle действия с помощью оператора SAVEPOINT, мы можем восстановить исходное поведение процедуры и при этом перехватывать и "игнорировать" ошибки.

Требования целостности и транзакции Интересно разобраться, когда именно проверяются требования целостности. По умолчанию требования целостности проверяются после выполнения всего SQL-оператора. Обратите внимание: я написал "SQL-оператора", а не просто "оператора". Если в хранимой процедуре PL/SQL есть несколько операторов SQL, проверка требований целостности каждого оператора выполняется сразу же по завершении его выполнения, а не Глава по завершении выполнения всей хранимой процедуры. Проверка требований целостности может быть программно отложена до завершения транзакции или до момента, когда разработчик сочтет необходимым их проверить. Итак, почему же требования проверяются после выполнения SQL-оператора, а не при выполнении? А потому, что одному оператору "позволено" кратковременно делать отдельные строки таблицы несогласованными. Попытка получить частичные результаты выполнения оператора приведет к отмене этих результатов сервером Oracle, даже если конечные результаты вполне допустимы. Пусть, например, имеется следующая таблица:

tkyte@TKYTE816> create table t (x int unique);

Table created. tkyte@TKYTE816> insert into t values (1);

1 row created. tkyte@TKYTE816> insert into t values (2) ;

1 row created.

Теперь, пусть необходимо выполнить изменение нескольких строк:

tkyte@TKYTE816> update t set x = x+1;

2 rows updated.

Если бы сервер Oracle проверял требование после изменения каждой строки, то вероятность неудачного завершения изменений составляла бы 50%. Строки в таблице Т просматриваются в определенном порядке;

поэтому, если бы сначала изменялась строка с Х=1, значение моментально дублировалось бы в столбце X, а изменение отвергалось. Поскольку сервер Oracle терпеливо ждет завершения оператора, он выполняется успешно, так как к моменту его завершения дублирующихся значений нет. Версии, начиная с Oracle 8.0, позволяют отложить проверку требований. Эта возможность может оказаться крайне полезной при выполнении различных операций. Например, в случае каскадного распространения изменения первичного ключа на внешние ключи. Многие скажут, что этого никогда не придется делать, что первичные ключи должны быть неизменны (и я тоже так скажу), но часто пользователи настаивают на возможности каскадного изменения. Отложенная проверка изменений позволяет это сделать. В предыдущих версиях в общем-то тоже можно было сделать каскадное изменение, но для этого приходилось выполнять огромный объем работы, да и сам подход имел определенные ограничения. С помощью отложенной проверки изменений задача становится практически тривиальной. Решение выглядит примерно так:

tkyte@TKYTE816> create table p 2 (pk int primary key) 3/ Table created. tkyte@TKYTE816> tkyte@TKYTE816> create table с Транзакции 2 3 4 5 6 7 Table (fk int constraint c_fk references p(pk) deferrable initially immediate ) / created.

tkyte@TKYTE816> insert into p values (1) ;

1 row created. tkyte@TKYTE816> insert into с values (1) ;

1 row created.

Итак, имеется родительская таблица Р и подчиненная таблица С. Таблица С ссылается на таблицу Р, а реализующее это правило требование названо C_FK (child foreign key — внешний ключ подчиненной таблицы). Это требование создано как допускающее отложенную проверку — DEFERRABLE, но при создании, кроме того, указана конструкция INITIALLY IMMEDIATE. To есть проверку требования можно отложить до фиксации транзакции или любого другого момента. По умолчанию, однако, проверка будет выполняться на уровне оператора. Именно так чаще всего и используются требования с отложенной проверкой. Существующие приложения не будут проверять нарушение требования перед выполнением оператора COMMIT, поэтому лучше не делать им таких сюрпризов. В соответствии с определением, таблица С будет вести себя обычным образом, но позволит явно это поведение изменить. Теперь давайте попытаемся применить ряд операторов ЯМД к таблицам и посмотрим, что получится:

tkyte@TKYTE816> update p set pk = 2;

update p set pk = * ERROR at line 1: ORA-02292: i n t e g r i t y constraint (TKYTE.C_FK) v i o l a t e d - c h i l d record found Поскольку требование сейчас работает в режиме IMMEDIATE, этот изменение не будет выполнено. Изменив режим требования, попробуем снова:

tkyte@TKYTE816> set constraint c_fk deferred;

Constraint set. tkyte@TKYTE816> update p set pk = 2;

1 row updated.

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

tkyte@TKYTE816> set constraint c_fk immediate;

set constraint с fk immediate Глава ERROR at line 1: ORA-02291: integrity constraint (TKYTE.C_FK) violated — parent key not found Этот оператор не выполняется и сразу же возвращает сообщение об ошибке, чего и следовало ожидать: мы ведь знаем, что требование нарушено. Изменение таблицы Р при этом не отменяется (это нарушило бы неделимость на уровне оператора). Учтите также, что транзакция по-прежнему работает с требованием C_FK в режиме DEFERRED, поскольку оператор SET CONSTRAINT не сработал. Продолжим, распространяя изменение на таблицу С:

tkyte@TKYTE816> update с set fk = 2;

1 row updated. tkyte@TKYTE816> set constraint c_fk immediate;

Constraint set. tkyte@TKYTE816> commit;

Commit complete.

Именно так это и происходит.

Плохие привычки при работе с транзакциями У многих разработчиков выработались плохие привычки в отношении транзакций. Особенно часто это наблюдается у разработчиков, которые имели дело с СУБД, поддерживающими транзакции, но не навязывающими их использование. Например, в СУБД Informix (по умолчанию), Sybase и SQL Server необходимо явно начинать транзакцию. В противном случае каждый оператор будет выполняться как отдельная транзакция. Как сервер Oracle неявно устанавливает вокруг отдельных операторов SAVEPOINT, в этих СУБД вокруг каждого оператора неявно устанавливаются операторы BEGIN WORK/COMMIT или ROLLBACK. Это объясняется тем, что в упомянутых СУБД блокировки — ценный ресурс, а сеансы, читающие данные, блокируют сеансы, изменяющие данные. Пытаясь повысить параллелизм, создатели этих СУБД заставляют разработчиков делать транзакции как можно короче (иногда в ущерб целостности данных). В СУБД Oracle принят другой подход. Транзакции всегда начинаются неявно, и возможность автоматической фиксации отсутствует, если только она не реализована в приложении (см. описание функционального интерфейса JDBC в конце этого раздела). В Oracle транзакция фиксируется, когда это необходимо, и не раньше. Транзакции должны быть такого размера, как нужно для приложения. Проблемы нехватки блокировок, невозможности доступа к данным для других сеансов и т.п. учитывать не надо — размер транзакции определяется исключительно требованиями обеспечения целостности данных. Блокировки не являются особо ценным ресурсом, а одновременное чтение и запись данных не приводит к конфликтам. Это обеспечивает надежность транзакций в базе данных. Продолжительность транзакций не ограничивается, — она определяется требованиями приложения. Транзакции не должны быть ориентированы на удобство работы Транзакции компьютера и его программного обеспечения — они предназначены для защиты целостности данных пользователей. Столкнувшись с задачей изменения большого количества строк, программисты обычно пытаются изобрести процедурный способ сделать это в цикле — так, чтобы можно было фиксировать изменения группы строк определенного размера. Я слышал два обоснования подобного решения: • быстрее и эффективнее фиксировать изменения часто с помощью множества небольших транзакций, чем обработать все строки и зафиксировать одну большую транзакцию;

• недостаточно места в сегментах отката. Оба эти утверждения неверны. Частые фиксации изменений не способствуют ускорению — почти всегда быстрее сделать все необходимое в одном SQL-операторе. Рассмотрим небольшой пример: имеется, например, таблица Т с множеством строк, и необходимо изменить значение в столбце для каждой строки этой таблицы. Это можно сделать одним простым оператором:

tkyte@TKYTE816> create table t as select * from all_objects;

Table created. tkyte@TKYTE816> set timing on tkyte@TKYTE816> update t set object_name = lower(object_name);

21946 rows updated. Elapsed: 00:00:01. Многие, однако, по разным причинам предпочитают делать это так:

tkyte@TKYTE816> begin 2 for x in (select rowid rid, object_name, rownum r 3 from t) 4 loop • 5 update t 6 set object name = lower(x.object_name) 7 where rowid = x.rid;

8 if (mod(x.r,100) = 0) then 9 commit;

10 end if;

11 end loop;

12 commit;

13 end;

14 / PL/SQL procedure successfully completed. Elapsed: 00:00:05. На этом простом примере показано, что частое фиксирование изменений в цикле в пять раз медленнее. Если что-то можно сделать одним SQL-оператором, делайте это именно так — наверняка получится быстрее.

Глава Глава Теперь давайте вернемся ко второй причине, связанной с экономным использованием разработчиком "ограниченного ресурса" (сегментов отката). Это проблема конфигурирования — необходимо предусмотреть достаточно места в сегментах отката для поддержки транзакций требуемого размера. Фиксация в цикле — мало того, что обычно медленнее, это одна из наиболее частых причин возникновения печально известной ошибки ORA-01555. Давайте рассмотрим эту проблему более детально. Как вы уже знаете из глав 1 и 3, в модели многовариантного доступа Oracle данные в сегментах отката используются для восстановления блоков в том виде, который они имели в начале выполнения оператора или транзакции (в зависимости от уровня изолированности). Если необходимые данные отмены недоступны, выдается сообщение об ошибке ORA-01555 snapshot too old, и запрос не выполняется. Так что, если вы изменяете считываемую вами таблицу (как в представленном выше анонимном блоке), то генерируете данные отмены, необходимые для выполнения запроса. Производимые изменения генерируют данные отмены, которые могут потребоваться при выполнении запроса для согласованного представления изменяемых данных. Если изменения фиксируются, системе разрешается повторно использовать только что занятое место в сегменте отката. Используя его, она стирает данные отката, которые в дальнейшем могут понадобиться для выполнения запроса, т.е. у вас возникает большая проблема. Оператор SELECT не выполнится, и изменение остановится на полпути. В результате получается частично завершенная транзакция, и никакого приемлемого способа выполнить ее повторно, как правило, не существует (об этом — чуть позже). Давайте рассмотрим это на маленьком примере. В небольшой тестовой базе данных я создал таблицу:

tkyte@TKYTE816> create table t as s e l e c t * from all_objects;

Table created. tkyte@TKYTE816> create index t_idx on t(object_name);

Index created.

Я затем отключил все сегменты отката и создал один маленький сегмент:

tkyte@TKYTE816> create rollback segment rbs_small storage (initial 64k 2 next 64k minextents 2 maxextents 4) tablespace tools;

Rollback segment created.

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

tkyte@TKYTE816> begin 2 for x in ( s e l e c t rowid rid, object_name, rownum r 3 from t 4 where object_name > chr(O)) 5 loop 6 update t 7 set object_name = lower(x.object_name) 8 where rowid = x.rid;

9 if (mod(x.r,100) = 0) then 10 commit;

11 end if;

Транзакции 12 end loop;

13 commit;

14 end;

15 / begin * ERROR at line 1: ORA-01555: snapshot too old: rollback segment number 10 with name "RBS_SMALL" too small ORA-06512: at l i n e Я получил сообщение об ошибке. Обращаю ваше внимание, что я добавил индекс и конструкцию WHERE, желая добиться случайности чтения строк таблицы. Конструкция WHERE приведет к использованию индекса (для этого я использовал оптимизатор, основанный на правилах). При доступе к таблице через индекс, обычно считывается блок, содержащий одну строку, а следующая необходимая строка оказывается уже в другом блоке. Мы обработаем все строки в блоке 1, просто не сразу. Пусть в блоке 1 находятся строки А, М, N, Q и Z. Так что придется обращаться к блоку пять раз через достаточно продолжительные интервалы времени. Поскольку изменения фиксируются часто и разрешено повторное использование пространства в сегменте отката, то в конце концов, повторно обратившись к блоку, который нельзя воспроизвести, мы получим сообщение об ошибке. Этот придуманный пример наглядно демонстрирует, как все происходит. Оператор UPDATE генерировал данные отката. У нас было четыре экстента размером 64 Кбайта в сегменте отката, т.е. всего 256 Кбайт. Мы многократно использовали сегмент оката, поскольку сегменты отката используются циклически. При каждой фиксации серверу Oracle разрешалось перезаписывать сгенерированные нами данные отката. В определенный момент понадобился нами же сгенерированный фрагмент данных, которого больше нет, и было получено сообщение об ошибке ORA-01555. Если не фиксировать изменения, то будет получено следующее сообщение об ошибке:

begin * ERROR at line 1: ORA-01562: failed to extend rollback segment number 10 ORA-01628: max # extents (4) reached for rollback segment RBS_SMALL ORA-06512: at line Но между этими двумя ошибками есть, однако, важные отличия. • Ошибка ORA-01555 оставляет изменения в абсолютно неизвестном состоянии. Часть изменений сделана, часть — нет. • Мы ничего не можем сделать, чтобы избежать ошибки ORA-01555, если зафиксировали изменения в цикле FOR по курсору. • Всегда можно избежать ошибки ORA-01562, выделив системе соответствующие ресурсы. Второй ошибки можно избежать, создав сегменты отката нужного размера, а первой — нет.

Глава Подведу итоги: нельзя "сэкономить" место в сегментах отката, фиксируя изменения чаше, — эта информация в сегментах отката необходима. (Работая в однопользовательской системе, я в течение одного сеанса получил сообщение об ошибке ORA-01555.) Разработчики и администраторы баз данных должны совместно определить необходимый размер сегментов отката для обычно выполняемых заданий. Путем анализа системы необходимо выяснить максимальный размер транзакций и создать для них соответствующие сегменты отката. В рассмотренном выше примере это может быть однократное изменение. В данном случае можно отдельно создать в системе большой сегмент отката исключительно для выполнения этого изменения. Затем с помощью оператора SET TRANSACTION заставить транзакцию использовать этот сегмент отката. По ее завершении сегмент отката можно удалить. Если подобная транзакция выполняется неоднократно, необходимо включить сегмент отката необходимого размера в систему на постоянной основе. Многие считают такие компоненты, как временные пространства, сегменты отката и журналы повторного выполнения, излишним расходованием ресурсов и выделяют для них минимум места на диске. Это заставляет вспомнить проблему, с которой столкнулась компьютерная индустрия 1 января 2000 года — и все из-за стремления сэкономить 2 байта в поле даты. Эти компоненты базы данных — ключевые в системе, и они должны иметь соответствующий размер (не большой и не маленький, а такой, как нужно). Конечно, самая большая проблема подхода с "фиксацией до завершения транзакции" состоит в том, что в случае прекращения изменений, база данных остается в неопределенном состоянии. Если заранее не подготовиться к этому, очень сложно продолжить выполнение неудачно завершившейся транзакции с момента останова. Например, если к столбцу применяется не функция LOWER(), а другая, например last_ddl_time = last_ddl_time + 1;

Если цикл изменения прерван по ходу, как нам его перезапустить? Нельзя просто выполнить его повторно, поскольку к некоторым датам будет добавлено значение 2, а к некоторым — 1. При повторном сбое окажется, что к некоторым добавлено значение 3, к другим — 2, к остальным — I и так далее. Необходим более сложный подход, позволяющий "фрагментировать" данные. Например, можно обрабатывать все значения object_names, начинающиеся с А, затем — начинающиеся с В, и так далее:

tkyte@TKYTE816> create table to_do 2 as 3 select distinct substr(object_name, 1, 1) first_char 4 from T 5 / Table created. tkyte@TKYTE816> begin 2 for x in (select * from to_do) 3 loop 4 update t set last_ddl_time = last_ddl_time+l 5 where object_name like x.first char || '%';

6 7 dbms_output.put_line(sql%rowcount || ' rows updated');

8 delete from to do where first char = x. first char;

Транзакции 9 10 commit;

11 end loop;

12 end;

13 / 11654 rows updated 21759 rows updated 309 rows updated 6 rows updated 270 rows updated 830 rows updated 412 rows updated 7 rows updated 378 rows updated 95 rows updated 203 rows updated 2482 rows updated 13 rows updated 318 rows updated 83 rows updated 14 rows updated 1408 rows updated 86 rows updated 2 rows updated 35 rows updated 2409 rows updated 57 rows updated 306 rows updated 379 rows updated 1 rows updated 1 rows updated PL/SQL procedure successfully completed.

Теперь в случае сбоя мы сможем перезапустить процесс, поскольку не будут обрабатываться имена объектов, уже успешно обработанных. Проблема этого подхода, однако, в том, что данные неравномерно распределены по обрабатываемым фрагментам. Второе изменение затронуло больше строк, чем все остальные вместе взятые. Кроме того, другие сеансы, обращающиеся к таблице и изменяющие данные, тоже могут изменить столбец object_name. Предположим, другой сеанс изменил имя объекта с Z на А после обработки фрагмента, содержащего объекты с именами на А — соответствующая запись будет пропущена. Более того, это — крайне неэффективный процесс по сравнению с использованием оператора update t set last_ddl_time = last_ddl_time+l. Для чтения каждой строки таблицы мы обычно используем индекс или многократно просматриваем всю таблицу, однако и то, и другое нежелательно. У этого подхода слишком много недостатков. Лучшим является подход, который я постоянно проповедую: делать проще. Если это можно сделать в SQL, делайте в SQL. To, что нельзя сделать одним SQL-оператором, делайте в PL/SQL. И используйте как можно меньше кода. Выделяйте достаточно ре Глава сурсов. Всегда думайте, что произойдет в случае ошибки. Часто разработчики пишут циклы для изменения данных, замечательно работающие в тестовой среде, но завершающиеся ошибкой после применения к реальным данным. И проблема здесь в том, что не известно, где остановилась обработка. Намного проще определить правильный размер сегмента отката, чем написать перезапускаемую транзакцию. Если должны изменяться большие таблицы, фрагментируйте их (подробнее об этом см. в главе 14), что позволит изменять каждый фрагмент отдельно. Для изменения можно даже использовать параллельные операторы ЯМД. И наконец, еще об одной плохой привычке при организации транзакций. Она приобретается при использовании популярных функциональных интерфейсов ODBC и JDBC. Эти функциональные интерфейсы по умолчанию выполняют "автоматическую фиксацию". Рассмотрим следующие операторы, переводящие 1000 $ с текущего счета на накопительный:

update accounts set balance = balance - 1000 where account_id = 123;

update accounts set balance = balance + 1000 where account_id = 456;

Если при эти операторы приложение отправляет через интерфейс JDBC, JDBC вставит оператор фиксации после каждого изменения. Подумайте о последствиях этого, если сбой произойдет после первого изменения, но перед вторым. Потеряны 1000 $! Я понимаю, почему интерфейс ODBC делает именно так. Интерфейс ODBC создавали разработчики SQL Server, а принятая в этой СУБД модель одновременного доступа (пишущие блокируют читающих, читающие — пишущих, и блокировки являются ограниченным ресурсом) требует использования очень коротких транзакций. Я не понимаю, как подобный подход был перенесен в JDBC — интерфейс, предназначавшийся для поддержки приложений "масштаба предприятия". Практика показала, что сразу же после подключения по интерфейсу JDBC необходимо выполнять следующую строку кода:

connection conn81 = DriverManager.getConnection ("jdbc:oracle:oci8:@ora8idev","scott","tiger");

conn81.setAutoCommlt (false);

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

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

Транзакции Ключевым для распределенных транзакций в Oracle является понятие связи базы данных (database link). Связь базы данных — это объект базы данных, описывающий, как подключиться к другому экземпляру с текущего. Однако этот раздел посвящен не синтаксису оператора создания связи (он подробно описан в документации). После создания связей базы данных доступ к удаленным объектам выполняется очень просто:

s e l e c t * from T@another_database;

Этот оператор выберет данные из таблицы Т в экземпляре, определяемом связью базы данных ANOTHER_DATABASE. Обычно факт удаленности таблицы Т "скрывают", создавая для нее представление или синоним. Например, можно выполнить:

create synonym T for T@another_database;

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

update local_table s e t x = 5;

update remote_table@another_database s e t у = 10;

commit;

И все. Сервер Oracle либо зафиксирует изменения в обеих базах данных, либо их не окажется ни в одной. Для этого используется протокол двухэтапной фиксации (two-phase commit — 2РС). 2РС — это распределенный протокол, позволяющий фиксировать неделимое изменение, затрагивающее несколько баз данных. Он пытается максимально ограничить возможность сбоя распределенной транзакции перед фиксацией. При использовании протокола 2РС одна из задействованных баз данных (обычно та, к которой первоначально подключен клиент) становится координатором распределенной транзакции. Сервер-координатор опрашивает соответствующие серверы, готовы ли они фиксировать изменения. Обращаясь к ним, он просит подготовиться к фиксации. Серверы сообщают о своем "состоянии готовности" в виде ДА или НЕТ. Если один из серверов сказал НЕТ, транзакция откатывается. Если все серверы сказали ДА, сервер-координатор рассылает сообщение о необходимости зафиксировать изменения на каждом сервере. Этот протокол ограничивает время, когда может произойти серьезная ошибка. Перед "голосованием" по протоколу двухэтапной фиксации любая ошибка на задействованном сервере приведет к откату транзакции на всех серверах. Таким образом, не будет никаких сомнений как в отношении исхода транзакции, так и в том, что касается состояния распределенной транзакции. Сомнения в случае сбоя возможны только очень непродолжительное время — при сборе ответов. Предположим, например, что в транзакции участвует три сервера;

сервер 1 — координатор. Сервер 1 попросил сервер 2 подготовиться к фиксации, и сервер 2 это сделал. Затем сервер 1 попросил сервер 3 подготовиться к фиксации, и тот подготовился. Сейчас только сервер 1 знает результат транзакции, и он обязан сообщить его двум другим серверам. Если в этот момент произойдет ошибка — сбой сети, сбой питания на сервере 1, любая ошибка, — серверы 2 и 3 останутся в "подвешенном" состоянии. У них возникнет так называемая сомнительная распределенная транзакция. Протокол двухэтапной фиксации пытается свести к ми Глава нимуму время, когда такая ошибка может произойти, но оно, тем не менее, не равно нулю. Серверы 2 и 3 должны держать транзакцию открытой в ожидании уведомления о результате от сервера 1. Если вспомнить архитектуру, описанную в главе 2, решать эту проблему должен процесс RECO. Здесь также вступают в игру операторы COMMIT и ROLLBACK с опцией FORCE. Если причиной проблемы был сбой сети между серверами 1, 2 и 3, то администраторы базы данных серверов 2 и 3 могут просто позвонить администратору базы данных сервера 1 и, в зависимости от результатов, выполнить соответствующий оператор фиксации или отката вручную. На действия, которые можно выполнять в распределенной транзакции, налагаются определенные ограничения. Этих ограничений, однако, немного, и все они обоснованы (мне, во всяком случае, они кажутся разумными). Вот основные ограничения. • Нельзя выполнять оператор COMMIT в удаленной базе данных. Фиксировать транзакцию можно только на сервере-инициаторе. • В удаленных базах данных нельзя выполнять операторы ЯОД. Это прямое следствие предыдущего ограничения на фиксацию операторов ЯМД. Фиксировать транзакцию можно только с сервера — инициатора транзакции, поэтому нельзя выполнять операторы ЯОД в связанных базах данных. • В удаленных базах данных нельзя выполнять оператор SAVEPOINT. Короче, в удаленных базах данных нельзя выполнять операторы управления транзакцией. Невозможность управлять транзакцией в удаленной базе данных вполне понятна, поскольку список серверов, задействованных в транзакции, есть только на сервере-инициаторе. В рассмотренной выше конфигурации с тремя серверами, если сервер 2 попытается зафиксировать транзакцию, он не сможет узнать, что в ней задействован сервер 3. В Oracle только сервер 1 может выполнять оператор фиксации. Но после этого сервер 1 может передать управление распределенными транзакциями другому серверу. Повлиять на то, какой именно сервер будет фиксировать транзакцию, можно путем установки приоритета точки фиксации сервера (с помощью параметра в файле init.ora). Приоритет точки фиксации (commit point strength) задает для сервера относительный уровень важности в распределенной транзакции: чем важнее сервер (чем более доступными должны быть его данные), тем больше вероятность, что он будет координировать распределенную транзакцию. Это может пригодиться в том случае, если необходимо организовать распределенную транзакцию между тестовым и производственным сервером. Поскольку координатор транзакции никогда "не сомневается" в результатах транзакции, предпочтительнее, чтобы координировал такую транзакцию производственный сервер. Ничего страшного, если ряд открытых транзакций и заблокированных ресурсов останется на тестовой машине. На производственном сервере этого допускать нельзя. Невозможность выполнять операторы ЯОД в удаленной базе данных, в общем, не так трагична. Во-первых, операторы ЯОД встречаются сравнительно редко. Они выполняются один раз после установки или после обновления программного обеспечения. На производственных системах в ходе работы операторы ЯОД не выполняются (по крайней мере не должны выполняться). Во-вторых, есть способ выполнить ЯОД в удаленной базе данных;

для этого с помощью пакета DBMS_JOB (он подробно описан в при Транзакции ложении А) формируется очередь заданий. Вместо того чтобы пытаться выполнить оператор ЯОД в удаленной базе данных, можно использовать связь для постановки в очередь задания, которое должно быть выполнено при фиксации транзакции. Такое задание выполняется на удаленной машине, но, не являясь распределенной транзакцией, вполне может включать операторы ЯОД. Именно так службы репликации Oracle выполняют распределенные операторы ЯОД при репликации схемы.

Журналы повторного выполнения и сегменты отката Завершить эту главу о транзакциях я хочу описанием того, как генерируются данные повторного выполнения и отката (отмены), и как они используются в транзакциях, при восстановлении и т.д. Этот вопрос мне часто задают. Понимание журнала повторного выполнения и сегментов отката, а также того, что происходит в процессе работы, поможет разобраться в функционировании базы данных в целом. Глубокое понимание того, что происходит при изменении данных, — так же важно для разработчиков, как и для администраторов баз данных. Необходимо знать последствия своих действий. Ниже представлен псевдокод, описывающий действие этих механизмов в Oracle. На самом деле происходящее — несколько сложнее, но пример поможет разобраться в сути происходящего. Посмотрим, что происходит при выполнении транзакции следующего вида:

insert into t (х,у) values (1,1);

update t set x = x+1 where x = 1;

delete from t where x = 2;

Разберем, что происходит при выполнении транзакции, по нескольким направлениям: • что происходит, если возникает сбой после изменения? • что происходит, если все операторы выполняются успешно и результаты фиксируются? • что происходит при откате? Первый оператор INSERT INTO T будет генерировать как данные повторного выполнения (redo), так и данные отмены (undo). Данных отмены будет достаточно, чтобы "избавиться" от последствий вставки. Данных повторного выполнения будет достаточно для того, чтобы повторить вставку. Данные отмены могут состоять из нескольких частей. Например, по столбцам X и Y могут быть индексы, и изменения в них также придется отменять при откате. Данные отмены хранятся в сегменте отката. Сегмент отката хранится в табличном пространстве и (это крайне важно) защищен журналом повторного выполнения, как и любой другой сегмент. Другими словами, данные отката обрабатываются так же, как и данные таблицы или индекса, — изменения в сегментах отката генерируют определенные данные повторного выполнения, которые записываются в журнал. (Зачем это делается, станет ясно после описания происходящего при сбое сие Глава темы). Данные отмены добавляются в сегмент отката и кэшируются в буферном кэше, как и любые другие данные. Как и генерируемые данные отката, данные повторного выполнения могу состоять из нескольких частей. Итак, после выполнения вставки мы имеем:

Буферный кэш Буфер журнала повторного выполнения Сегмент отката Индексы ТаблицаТ В кэше имеются измененные блоки сегмента отката, блоки индекса и блоки данных таблицы. Каждый из измененных блоков защищен записями в буфере журнала повторного выполнения. Вся эта информация пока кэширована в памяти. Гипотетический сценарий: сбой системы происходит сейчас. Все в порядке. Содержимое области SGA пропало, но хранившееся там нам не нужно. При перезапуске все пойдет так, будто транзакция никогда не выполнялась. Ни один из блоков с изменениями не сброшен на диск;

не сброшены и данные повторного выполнения. Гипотетический сценарий: прямо сейчас заполняется буферный кэш. Процесс DBWR должен освободить место и сбросить на диск только что измененные блоки. В этом случае процесс DBWR сначала попросит процесс LGWR сбросить на диск блоки журнала повторного выполнения, защищающие блоки базы данных. Прежде чем процесс DBWR сможет записать на диск измененные блоки, процесс LGWR должен сбросить на диск данные повторного выполнения, связанные с этими блоками. В этом есть смысл, так как, сбросив на диск измененные блоки таблицы Т и не сбросив при этом данные повторного выполнения для соответствующих блоков отмены, в случае сбоя системы мы получим измененный блок таблицы Т, для которого нет соответствующих данных отмены. Нужно сбросить на диск буферы журнала повторного выполнения, прежде чем записывать туда эти блоки, чтобы при необходимости отката можно было повторно выполнить все изменения для перевода области SGA в текущее состояние. Второй сценарий поможет продемонстрировать, что привело к созданию подобной системы. Набор условий "если мы сбросили блоки таблицы Т, но не сбросили данные повторного выполнения для блоков отмены, и произошел сбой системы" становится сложным. При добавлении пользователей, дополнительных объектов и одновременной обработки все станет еще сложнее. Итак, имеется ситуация, представленная на предыдущем рисунке. Мы сгенерировали ряд измененных блоков таблицы и индекса. В результате был создан ряд новых блоков в сегменте отката, и все три типа блоков сгенерировали определенный объем защи Транзакции щающих их данных повторного выполнения. Если вспомнить буфер журнала повторного выполнения, обсуждавшийся ранее, — он сбрасывается каждые три секунды, при заполнении на треть и при обработке контрольной точки. Весьма вероятно, что в определенный момент буфер журнала повторного выполнения будет сброшен на диск, и некоторые из сделанных нами изменений тоже окажутся на диске. В этом случае схема приобретает следующий вид: Буферный кэш Буфер журнала повторного выполнения Сегмент отката Индексы ТаблицаТ Журнал повторного выполнения Теперь на диске могут оказаться и блоки, но мы этот случай не рассматриваем. Затем мы изменяем данные. Происходит примерно то же самое. В этот раз объем данных отмены больше (теперь в результате изменения нам надо сохранить ряд "предварительных" образов). Имеем следующую картину:

Буферный кэш Сегмент отката Индексы Таблица Т Журнал ПОВТОРНОГО выполнения В буферном кэше добавились новые блоки сегмента отката. Чтобы при необходимости отменить изменение, можно воспользоваться имеющимися в кэше измененными блоками таблицы и индекса. Сгенерированы также дополнительные записи в буфер журнала повторного выполнения. Часть сгенерированных данных повторного выполнения находится на диске, часть — в кэше. Гипотетический сценарий: сбой системы происходит в этот момент. При запуске сервер Oracle будет считывать журналы повторного выполнения и обнаружит ряд записей повторного выполнения транзакции. С учетом состояния системы (записи повторного выполнения для оператора вставки находятся в файлах журнала на диске, а записи повторного выполнения для оператора изменения — все еще в буфере) сервер Oracle "на Глава катит" (roll forward) вставку. В результате получится схема, аналогичная первой: в памяти имеется ряд блоков отмены из сегмента отката (для отмены вставки), измененные (после вставки) блоки таблицы и измененные (после вставки) блоки индекса. Теперь сервер Oracle обнаружит, что транзакция не зафиксирована, и откатит ее, поскольку выполняется восстановление после сбоя, а наш сеанс, конечно же, не подключен. Сервер прочитает данные отмены, помещенные при накате в буферный кэш, и применит к блокам данных и индексов, приводя их к состоянию, которое было до вставки. Все вернулось на свои места. Блоки на диске могут отражать (или не отражать) результаты выполнения оператора INSERT (это зависит от того, были ли они сброшены на диск перед сбоем). Если были, значит, вставка отменена, и при сбросе блоков из буферного кэша на диск эта отмена будет отражена и в файлах данных. Если же результатов вставки в этих блоках нет, — пусть так и будет, их все равно потом перезапишут. Гипотетический сценарий: приложение откатывает транзакцию. В этот момент сервер Oracle находит данные отмены для транзакции либо в блоках сегмента отката в кэше (скорее всего), либо на диске, если они уже были сброшены (это более вероятно в случае очень больших транзакций). Он применяет данные отмены к блокам данных и индекса в буферном кэше;

если их в кэше уже нет, они считываются с диска в кэш, после чего к ним применяются данные отмены. Эти блоки в дальнейшем будут сброшены в файлы данных в исходном виде. Первый сценарий затрагивает некоторые детали восстановления в случае сбоя. Сервер выполняет его за два шага. Сначала он накатывает изменения, приводя систему к состоянию на момент сбоя, после этого откатывается все, что не было зафиксировано. Это действие повторно синхронизирует файлы данных. Сервер повторно выполняет все действия, а затем отменяет все, что не было завершено. Со вторым сценарием мы сталкиваемся намного чаще. Следует помнить, что в ходе отката журналы повторного выполнения никогда не используются. Журналы повторного выполнения читаются только при восстановлении и архивировании. Это ключевая концепция настройки: журналы повторного выполнения — только для записи. В ходе обычной обработки сервер Oracle их не читает. Если имеется достаточное количество устройств, т.е. при считывании процессом ARCH файла процесс LGWR выполняет запись на другое устройство, конфликты доступа к журналам повторного выполнения не возникают. Во многих других СУБД файлы журналов используются, как "журналы транзакций". В них нет разделения данных повторного выполнения и отмены — и те, и другие хранятся в одном файле. Для таких систем откат может оказаться катастрофой: в процессе отката должны считываться журналы, в которые выполняет запись процесс, сбрасывающий буферы журнала. В результате конфликты возникают в той части системы, где они наименее желательны. Разработчики Oracle постарались сделать так, чтобы журналы записывались последовательно и никто не читал бы их в процессе записи. Теперь переходим к оператору DELETE. И в этом случае генерируются данные отмены, блоки изменяются, а записи повторного выполнения отправляются в буфер журнала повторного выполнения. Это мало отличается от обработки оператора UPDATE, рассмотренной ранее, поэтому перейдем к оператору COMMIT. При обработке этого оператора сервер Oracle сбросит буфер журнала повторного выполнения на диск, и схема происходящего будет выглядеть примерно так:

Транзакции Буферный кэш Сегмент отката Индексы ТаблицаТ Журнал ПОВТОРНОГО выполнения Измененные блоки находятся в буферном кэше;

возможно, некоторые из них были сброшены на диск. Все данные повторного выполнения, необходимые для восстановления транзакции, находятся в безопасности на диске, и теперь изменения стали постоянными. Если придется считывать данные непосредственно из файлов данных, то блоки будут получены в том состоянии, в котором они находились перед началом транзакции, поскольку процесс DBWR их скорее всего еще не записал. Это нормально: в случае сбоя записи в журнале повторного выполнения можно будет использовать для восстановления блоков. Данные отмены будут сохраняться еще некоторое время — до повторного использования сегмента отката и перезаписи соответствующих блоков. Сервер Oracle с помощью этих данных отмены обеспечит согласованное чтение измененных объектов любыми сеансами, которым это надо.

Резюме В этой главе мы рассмотрели различные аспекты управления транзакциями в Oracle. Поддержка транзакций — одна из основных особенностей, отличающих базу данных от файловой системы. Понимание их механизма и способов использования необходимо для создания корректно работающих приложений для любой СУБД. Понимание того, что в Oracle все операторы (включая их побочные эффекты) — неделимы и что эта неделимость распространяется на хранимые процедуры, имеет принципиальное значение. Мы показали, как добавление обработчика исключительных ситуаций WHEN OTHERS в блок PL/SQL может радикально повлиять на изменения в базе данных. Для разработчиков приложений глубокое понимание работы транзакций принципиально важно. Мы рассмотрели весьма сложное взаимодействие требований целостности (уникальных ключей, требований проверки и т.д.) и транзакций в Oracle. Было описано, что сервер Oracle обычно обрабатывает требования целостности сразу после выполнения оператора, но при желании можно отложить проверку требований до конца транзакции. Эта возможность является ключевой при выполнении сложных многотабличных изменений, когда изменяемые таблицы — взаимозависимы. В качестве примера рассматривалось каскадное изменение. Затем были описаны вредные привычки, вырабатываемы в процессе работы с базами данных, "поддерживающими", но не "навязывающими" использование транзакций.

Глава Изложено основное правило организации транзакций: они могут быть короткими или длинными — это определяется необходимостью. Размер транзакции определяется требованием целостности данных — вот основная мысль, которую мы пытались донести до вас в этой главе. Только установленные в системе правила обработки данных должны определять размер транзакций, но никак не размер сегментов отката и не количество блокировок. Рассмотрены распределенные транзакции и их отличие от транзакций, затрагивающих только одну базу данных. Описаны ограничения, налагаемые на распределенные транзакции, и объяснены причины этого. Прежде чем создавать распределенную систему, необходимо понимать эти ограничения. То, что работает на одном сервере, может не работать в распределенной базе данных. Глава завершается описанием использования данных повторного выполнения и отмены, архитектурных особенностей, позволяющих обеспечить обсуждавшиеся в начале главы свойства транзакций (ACID). Изменение данных в транзакции было описано с точки зрения генерации данных для повторного выполнения и отмены (данных отката). В следующей главе это описывается очень подробно.

Повторное выполнение и откат В главе 4 мы рассмотрели основы повторного выполнения и отката (отмены) транзакций. Там было описано, что такое данные повторного выполнения. Это данные, которые сервер Oracle записывает в активные файлы журнала для повторного выполнения транзакций в случае сбоя. Они позволяют серверу Oracle восстанавливать выполненные транзакции. Мы также рассматривали данные отмены, или отката, которые сервер Oracle записывает в сегменты отката для отмены, или отката, транзакции. Кроме того, мы затронули несколько проблем, например причины возникновения ошибки ORA-01555: snapshot too old или прекращения обработки контрольной точки (checkpoint not complete, cannot allocate new log). В этой главе я хочу более глубоко рассмотреть суть процессов повторного выполнения и отката, а также описать, что должны знать о них разработчики. Повторное выполнение и откат — это темы, одинаково важные для администратора базы данных и для разработчиков. И тем, и другим необходимо глубокое понимание их назначения, алгоритмов работы, а также знание того, как избегать связанных с этими процессами проблем. Эта информация будет представлена в данной главе. Мы не будем касаться средств, использование и настройка которых является исключительной прерогативой администратор базы данных. Например, мы не будем рассматривать, как найти оптимальное значение параметров инициализации RECOVERY_PARALLELISM или FAST_START_IO_TARGET. Мы сконцентрируемся на проблемах, о которых должен знать разработчик, и на том, как они могут влиять на приложение.

Глава Повторное выполнение Файлы журнала повторного выполнения чрезвычайно важны для базы данных Oracle. Это журналы транзакций базы данных. Они используются только для восстановления;

единственное их назначение — предоставить необходимую информацию в случае сбоя экземпляра или носителя. Если на машине, где работает сервер, пропадает питание, что приводит к сбою экземпляра, сервер Oracle будет использовать активные журналы повторного выполнения для восстановления системы в состояние на момент, непосредственно предшествующий отключению питания. Если происходит сбой диска, сервер Oracle будет использовать архивные журналы повторного выполнения для восстановления резервной копии этого диска на соответствующий момент времени. Кроме того, если случайно удалена таблица или важные данные и это изменение зафиксировано, можно восстановить потерянные данные с резервной копии, а затем с помощью активного и архивных файлов журнала повторного выполнения привести их в состояние, соответствующее моменту, непосредственно предшествующему этому случаю. Сервер Oracle поддерживает два типа файлов журнала повторного выполнения: активные и архивные. В каждой базе данных Oracle есть по крайней мере два активных файла журнала повторного выполнения. Эти активные файлы журнала повторного выполнения используются циклически. Сервер Oracle будет выполнять запись в журнальный файл 1, а добравшись до конца этого файла, переключится на журнальный файл 2 и начнет записывать в него. Заполнив журнальный файл 2, он опять переключится на журнальный файл 1 (если файлов журнала повторного выполнения всего два;

если их три, сервер, конечно же, переключится на третий файл). Архивные файлы журнала повторного выполнения — это просто копии старых, заполненных активных файлов журнала повторного выполнения. Когда система заполняет журнальные файлы, процесс ARCH копирует активный файл журнала повторного выполнения в другое место. Архивные файлы журнала повторного выполнения используются для восстановления носителя, когда сбой приводит к порче диска или в случае другого физического сбоя. Сервер Oracle может применять эти архивные файлы журнала повторного выполнения к резервным копиям файлов данных, чтобы привести их в соответствие с остальной базой данных. Эти журналы содержат хронологию транзакций в базе данных. Поддержка журналов повторного выполнения, или журналов транзакций, — одна из основных особенностей баз данных. Эти журналы, пожалуй, — самая важная структура, обеспечивающая восстановление, хотя без остальных компонентов, таких как сегменты отката, средства восстановления распределенных транзакций и т.п. тоже ничего не получится. Именно эти основные компоненты отличают базу данных от обычной файловой системы. Активные журналы повторного выполнения позволяют эффективно восстанавливать данные после сбоя питания, которое может произойти в тот момент, когда сервер Oracle выполняет запись. Архивные журналы повторного выполнения позволяют восстановить данные в случае выхода из строя носителей, например в случае поломки жесткого диска. Без журналов повторного выполнения база данных обеспечивала бы не большую защиту, чем файловая система. Важно понять, какое значение файлы журнала имеют для разработчиков. Мы рассмотрим, как различные способы написания кода влияют на использование журналов.

Повторное выполнение и откат Разберемся, почему возникают некоторые ошибки в базе данных (в частности, ORA01555: snapshot too old) и как их предотвратить. Мы уже рассматривали механизм повторного выполнения в главе 4, а теперь затронем ряд специфических проблем. Многие из этих проблем могут выявить разработчики, но исправлять их должен администратор базы данных, поскольку они влияют на весь сервер в целом. Мы начнем с анализа происходящего в ходе фиксации, а затем перейдем к часто задаваемым вопросам и проблемам, связанным с активными журналами повторного выполнения.

Что происходит при фиксации?

Разработчики должны очень четко понимать, что происходит при выполнении оператора COMMIT. Фиксация транзакции — очень быстрая операция, независимо от размера транзакции. Можно подумать, что чем больше транзакция (т.е. чем больше данных она затрагивает), тем дольше будет выполняться фиксация. Это неверно. Время выполнения фиксации обычно достаточно стабильно и мало зависит от размера транзакции. Дело в том, что при фиксации не так уж много делается, но эти действия — жизненно важны. Понимая все это, вы сможете делать транзакции настолько большими, как они должны быть. Многие разработчики искусственно ограничивают размер своих транзакций, фиксируя строки группами, а не после выполнения логической единицы работы. Они делают это, исходя из ошибочного предположения, что экономят ресурсы системы;

на самом же деле они их транжирят. Если фиксация одной строки требует X единиц времени, а фиксация тысячи строк — тех же X единиц времени, то, выполняя работу так, что 1000 строк фиксируется одним оператором COMMIT, можно сэкономить 999 единиц времени. Фиксируя транзакции только при необходимости (когда транзакция закончена), вы не только повышаете производительность, но и сокращаете конфликты доступа к общим ресурсам (журнальным файлам, внутренним защелкам и т.п.). Простой пример демонстрирует, что короткие транзакции требуют для выполнения больше времени:

tkyte@TKYTE816> create table t (x int) ;

tkyte@TKYTE816> set serveroutput on tkyte@TKYTE816> declare 2 l_start number default dbms_utility.get_time;

3 begin 4 for i in 1.. 1000 5 loop 6 insert into t values (1) ;

7 end loop;

8 commit;

9 dbms_output.put_line 10 (dbms_utility.get_time-l_start || ' hsecs');

11 end;

12 / 7 hsecs PL/SQL procedure successfully completed. tkyte@TKYTE816> declare Глава 5 l_start number default dbms_utility.get_time;

for i in 1.. 1000 loop insert into t values (1) ;

commit;

end loop;

dbms_output.put_line (dbms_utility.get_time-l_start || ' hsecs');

2 3 begin 4 5 6 7 8 9 10 11 end;

12 / 21 hsecs PL/SQL procedure successfully completed.

В данном случае потребовалось в три раза больше времени (в вашем случае результат может быть другим). При одновременном выполнении этого кода несколькими пользователями, слишком часто фиксирующими транзакции, скорость работы существенно уменьшается. Мной было показано, как не использование связываемых переменных и частое выполнение жесткого разбора заметно снижает параллелизм из-за конфликтов доступа к библиотечному кэшу и дополнительной нагрузки на процессор. Даже с учетом использования связываемых переменных слишком частое выполнение мягких разборов существенно увеличивает расходы ресурсов. Операции надо выполнять, только когда это действительно необходимо, а фиксация транзакции — такая же операция, как и разбор. Размер транзакций должен определяться требованиями приложения, а не ошибочными попытками сэкономить ресурсы базы данных. Так почему же продолжительность фиксации почти не зависит от размера транзакции? До начала фиксации изменений в базе данных, мы уже сделали все самое сложное — изменили данные, так что 99,9 процента работы сделано. Например, уже были выполнены следующие операции: • в области SGA сгенерированы записи сегмента отката;

• в области SGA сгенерированы измененные блоки данных;

• помещены в буфер журнала повторного выполнения в области SGA данные повторного выполнения для перечисленных выше изменений;

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

• установлены все необходимые блокировки. При выполнении фиксации осталось сделать следующее. • Сгенерировать номер системного изменения (SCN — System Change Number) для транзакции. • Процессу LGWR надо записать на диск все оставшиеся записи из буфера журнала повторного выполнения, а также записать в активные файлы журнала повторного выполнения номер SCN. Именно этот шаг и является фактической фиксацией. Если этот шаг выполнен, — транзакция зафиксирована. Если запись Повторное выполнение и откат транзакции удалена, значит, транзакция зафиксирована. Соответствующая запись в представлении V$TRANSACTION исчезнет. • Все блокировки, удерживаемые транзакцией, снимаются, и все сеансы, ожидавшие в очередях снятия этих блокировок, могут продолжить работу. • Многие измененные транзакцией блоки данных будут повторно обработаны и "очищены" в быстром режиме, если они еще находятся в буферном кэше. Как видите, для обработки оператора COMMIT надо сделать очень немного. Самая продолжительная операция выполняется процессом LGWR, поскольку связана с физическим вводом/выводом. Время работы процесса LGWR при этом будет в рамках допустимого (ограничено), поскольку он периодически сбрасывает на диск содержимое буфера журнала повторного выполнения. Процесс LGWR не буферизует все изменения по ходу их выполнения. Он постепенно сбрасывает содержимое буфера журнала повторного выполнения в фоновом режиме по мере его заполнения. Так делается, чтобы при выполнении оператора COMMIT не пришлось очень долго ждать разового сброса всей сгенерированной информации повторного выполнения. Процесс LGWR выполняет этот сброс постоянно: • каждые три секунды;

• при заполнении буфера журнала на треть или при записи в него 1 Мбайт информации;

• при фиксации транзакции. Так что, даже если транзакция выполняется долго, большая часть сгенерированной информации повторного выполнения уже сброшена на диск еще до фиксации. С другой стороны, однако, при фиксации необходимо дождаться, пока вся оставшаяся в буфере сгенерированная информация повторного выполнения не будет сохранена на диске. Таким образом, обращение к процессу LGWR выполняется синхронно. Хотя процесс LGWR и может использовать асинхронный ввод/вывод для параллельной записи в файлы журналов повторного выполнения, транзакция будет ждать, пока процесс LGWR не закончит все записи и не получит подтверждение записи всех данных на диск. Если вам еще не знаком упомянутый выше номер SCN — это простой механизм отслеживания времени, используемый сервером Oracle для упорядочения транзакций и возможности восстановления после сбоя. Он также используется для обеспечения согласованности по чтению и при обработке контрольной точки в базе данных. Номер SCN можно рассматривать как счетчик;

при каждой фиксации транзакции значение SCN увеличивается на единицу. Осталось прояснить одно: что значит "очистить" блоки базы данных. Я написал, что некоторые блоки, измененные транзакцией, будут повторно обработаны и очищены в процессе фиксации. Имеется в виду информация о блокировках, хранящаяся в заголовках блоков данных. Далее, в разделе "Очистка блоков", этот процесс будет описан подробно. Если коротко, происходит удаление данных транзакции из блока, чтобы следующий посетитель блока был избавлен от необходимости делать это. При этом не генерируются данные в журнал повторного выполнения, что существенно экономит ресурсы.

Глава Чтобы продемонстрировать, что продолжительность фиксации не зависит от размера транзакции, я буду генерировать различные объемы данных повторного выполнения и замерять время работы операторов INSERT и COMMIT. Для этого необходимо получить привилегии доступа к представлениям V$ (подробнее об этих представлениях см. в главе 10). Получив эти привилегии, мы создадим достаточно большую тестовую таблицу. В данном случае для генерации строк данных я использую представление ALL_OBJECTS и вставлю несколько экземпляров соответствующих строк, чтобы в итоге получилось порядка 100000 строк для работы (для получения такого же количества строк вам, возможно, придется выполнить операторы INSERT другое количество раз):

tkyte@TKYTE816> connect sys/change_on_install sys@TKYTE816> grant select on v_$mystat to tkyte;

Grant succeeded. sys@TKYTE816> grant select on v_$statname to tkyte;

Grant succeeded. sys@TKYTE816> connect tkyte/tkyte tkyte@TKYTE816> drop table t;

Table dropped. tkyte@TKYTE816> create table t 2 as 3 select * from all_objects 4 / Table created. tkyte@TKYTE816> insert into t select * from t;

21979 rows created. tkyte@TKYTE816> insert into t select * from t;

43958 rows created. tkyte@TKYTE816> insert into t select * from t where rownum < 12000;

11999 rows created. tkyte@TKYTE816> commit;

Commit complete. tkyte@TKYTE816> create or replace procedure do_commit(p_rows in number) 2 as 3 l_start number;

4 l_after_redo number;

5 l_before_redo number;

6 begin 7 select v$mystat.value into l_before_redo 8 from v$mystat, v$statname 9 where v$mystat.statistic# = v$statname.statistic# 10 and v$statname.name = 'redo size';

11 12 l_start := dbms_utility.get_time;

13 insert into t select * from t where rownum < p_rows;

Повторное выполнение и откат 14 dbms_output.put_line 15 (sql%rowcount || ' r o w s c r e a t e d ' ) ;

16 dbms_output.put_line 17 (' T i m e to I N S E R T : ' || 18 to_char(round((dbms_utility.get_time-l_start)/100, 5 ), 19 ' 9 9 9. 9 9 ' ) || 20 ' seconds');

21 22 l _ s t a r t := d b m s _ u t i l i t y. g e t _ t i m e, 23 commit;

24 dbms_output.put_line 25 ('Time to C O M M I T : ' || 26 to_char(round((dbms_utility.get_time-l_start)/100, 5 ), 27 '999.99') || 28 ' seconds');

29 30 select v$mystat.value into l_after_redo 31 from v$mystat, v$statname 32 where v$mystat.statistic# = v$statname.statistic# 33 a n d v $ s t a t n a m e. n a m e = 'redo s i z e ' ;

34 35 dbms_output.put_line 36 ( ' G e n e r a t e d ' || 37 to_char(l_after_redo-l_before_redo,'999,999,999,999') || 38 ' b y t e s of r e d o ' ) ;

39 dbms_output.new_line;

40 end;

41 / Procedure created.

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

tkyte@TKYTE816> set serveroutput on format wrapped tkyte@TKYTE816> begin 2 for i in 1.. 5 3 loop 4 do_commit(power(10, i)) ;

5 end loop;

6 end;

7/ 9 rows created Time to INSERT:.06 seconds Time to COMMIT:.00 seconds Generated 1,512 bytes of redo 99 rows created Time to INSERT: Time to COMMIT: Generated.06 seconds.00 seconds 11,908 bytes of redo Глава 999 rows created Time to INSERT: Time to COMMIT: Generated.05 seconds.00 seconds 115,924 bytes of redo 9999 rows created Time to INSERT:.46 seconds Time to COMMIT:.00 seconds Generated 1,103,524 bytes of redo 99999 rows created Time to INSERT: 16.36 seconds Time to COMMIT:.00 seconds Generated 11,220,656 bytes of redo PL/SQL procedure successfully completed. tkyte@TKYTE816> show parameter log_buffer NAME log buffer TYPE integer VALUE Как видите, при генерации разного объема данных повторного выполнения, от 1512 байт до 11220656 байт, время выполнения оператора COMMIT пренебрежимо мало — менее одной сотой секунды. Я выдавал время выполнения операторов INSERT специально, чтобы продемонстрировать, что алгоритм таймера "работает". Если делается чтото, занимающее определенное время, таймер это показывает, просто операторы COMMIT выполняются слишком быстро. По ходу обработки, по мере генерации данных в журнал повторного выполнения процесс LGWR постоянно сбрасывал данные из буфера журнала на диск в фоновом режиме. Так что, даже когда мы сгенерировали 11 Мбайт данных повторного выполнения, процесс LGWR постоянно сбрасывал ее на диск порциями примерно по 170 Кбайт (треть от 512000 байт). Когда дошло до выполнения оператора COMMIT, осталось сделать не так уж много — не намного больше, чем при вставке девяти строк данных. Можно ожидать подобных (но не точно таких же) результатов, независимо от объема сгенерированных данных повторного выполнения.

Что происходит при откате?

Если заменить оператор COMMIT оператором ROLLBACK результат будет совершенно другим. Время отката будет зависеть от объема измененных данных. Я изменил созданную в предыдущем разделе процедуру DO_COMMIT так, чтобы она выполняла откат (просто заменил оператор COMMIT в строке 23 оператором ROLLBACK) и получил совсем другое время выполнения. Например:

9 rows created Time to INSERT: Time to ROLLBACK: Generated.06 seconds.02 seconds 1,648 bytes of redo Повторное выполнение и откат 99 rows created Tine to INSERT: Time to ROLLBACK: Generated 999 rows created Time to INSERT: Time to ROLLBACK: Generated.04 seconds.00 seconds 12,728 bytes of redo.04 seconds.01 seconds 122,852 bytes of redo 9999 rows created Time to INSERT:. 94 seconds Time to ROLLBACK:.08 seconds Generated 1,170,112 bytes of redo 99999 rows created Time to INSERT: 8.08 seconds Time to ROLLBACK: 4.81 seconds Generated 11,842,168 bytes of redo PL/SQL procedure successfully completed.

Это вполне можно было предсказать, поскольку при откате приходится физически отменять сделанные изменения. Как и при фиксации, необходимо выполнить ряд операций. Прежде чем добраться до оператора ROLLBACK, сервер уже многое сделал. Напомню, что было сделано следующее: • в области SGA сгенерированы записи сегмента отката;

• в области SGA сгенерированы измененные блоки данных;

• помещены в буфер журнала повторного выполнения в области SGA данные повторного выполнения для перечисленных выше изменений;

• в зависимости от размера этих трех фрагментов информации и прошедшего времени, часть их может уже быть сброшена на диск;

• установлены все необходимые блокировки. При откате: • Отменяются все сделанные изменения. Это достигается путем считывания данных из сегмента отката и выполнения обратной операции. Если строка была вставлена, при откате она будет удалена. Если строка изменена, при откате будут восстановлены прежние значения столбцов. Если строка была удалена, при откате она будет снова вставлена. • Снимаются все блокировки, удерживаемые сеансом, и все сеансы, ждавшие в очереди снятия этих блокировок, могут продолжить работу. При фиксации же просто сбрасываются на диск все данные, оставшиеся в буфере журнала повторного выполнения. По сравнению с откатом, действий при этом выполняется очень мало. Идея в том, что откатывать транзакцию не надо, если в этом нет крайней необходимости. Это влечет большие расходы ресурсов системы, поскольку сна Глава чала много времени уходит на выполнение изменений, а потом — на их отмену. Не делайте никаких изменений, если не собираетесь их фиксировать. Это кажется очевидным: естественно, никто ничего не делает, если не собирается результат фиксировать. Я, однако, неоднократно видел, как разработчики используют "реальную" таблицу в качестве временной, наполняя ее данными, а затем откатывая изменения. Ниже мы еще обсудим реальные временные таблицы и как избежать подобной проблемы.

Какой объем данных повторного выполнения генерируется?

Разработчикам может потребоваться определить объем генерируемых операциями данных повторного выполнения. Чем больше данных повторного выполнения генерируется, тем дольше будут выполняться операции и тем медленнее будет работать система в целом. Вы влияете не только на свой сеанс, но и на все сеансы. Управление данными повторного выполнения в базе данных осуществляется последовательно — рано или поздно все сеансы обращаются к процессу LGWR, чтобы он обработал их данные повторного выполнения и зафиксировал транзакцию. Чем больше ему надо сделать, тем медленнее будет работать система. Зная, сколько информации повторного выполнения генерируется, и проверяя несколько подходов к решению проблемы, можно найти наилучший вариант. Как было показано выше, определить объем генерируемой информации повторного выполнения достаточно просто. Я использовал представление динамической производительности V$MYSTAT, содержащее статистику только моего сеанса, и соединил его с представлением V$STATNAME. Я выбрал из него значение статистического показателя redo size. Мне не пришлось угадывать имя этого показателя — его легко найти в представлении V$STATNAME:

ops$tkyte@DEV816> select * from v$statname 2 where name like 'redo%';

STATISTIC# 61 62 98 99 100 101 102 103 104 105 106 107 108 109 NAME redo redo redo redo redo redo redo redo redo redo redo redo redo redo synch writes synch time entries size buffer allocation retries wastage writer latching time writes blocks written write time log space requests log space wait tine log switch interrupts ordering marks CLASS 8 8 2 2 2 2 2 2 2 2 2 2 2 14 rows selected.

Повторное выполнение и откат Теперь можно приступать к изучению способа определения объема генерируемых транзакцией данных повторного выполнения. Достаточно легко оценить, сколько данных повторного выполнения будет сгенерировано, если известно, сколько данных будет изменено. Ниже я создаю таблицу, длина строки которой — около 2010 байт, плюсминус несколько байт. Поскольку данные типа CHAR всегда имеют максимальный размер, в строке 2000 байт занимает столбец типа CHAR, 7 байт — столбец типа DATE, и три байта необходимо для представления числа — всего получается около 2010 байт, плюс некоторое количество" байтов на представление строки:

tkyte@TKYTE816> create table t (x int, у char(2000), z date);

Table created.

Рассмотрим, сколько данных повторного выполнения будет сгенерировано при вставке, затем — при изменении и, наконец, — при удалении одной, десяти и множества таких строк. Разберемся также, есть ли существенное различие между одновременным изменением строк и по одной, основываясь на объеме генерируемых данных повторного выполнения. Мы уже знаем, что изменение строк по одной выполняется медленнее, чем с помощью одного оператора UPDATE. Для измерения объема генерируемых данных повторного выполнения можно использовать средство AUTOTRACE утилиты SQL*Plus либо непосредственный запрос к представлениям V$MYSTAT/V$STATNAME, показывающий объем данных повторного выполнения сеанса:

tkyte@TKYTE816> create or replace view redo_size 2 as 3 select value 4 from v$mystat, v$statname 5 where v$mystat.statistic# = v$statname.statistic# 6 and v$statname.name = 'redo size';

View created.

Для операторов, трассируемых при установке AUTOTRACE (операторов INSERT, UPDATE и DELETE), мы будем использовать выдаваемую информацию. Для блоков PL/ SQL придется обращаться к представлениям V$MYSTAT/V$STATNAME, поскольку AUTOTRACE для них такую информацию генерировать не будет. Для оценки объема генерируемых данных повторного выполнения будем использовать созданную ранее таблицу Т со сравнительно постоянным размером строки 2010 байт, если все столбцы не могут иметь пустых значений. Будем выполнять различные операции и измерять объем сгенерированных при этом данных повторного выполнения. Вставим сначала одну строку, затем — десять строк с помощью одного оператора, затем — 200 строк также с помощью одного оператора и, наконец, 200 строк — по одной. Аналогичную проверку выполним для изменения и удаления строк. Ниже представлен код для этого примера. Вместо обычного копирования и вставки кода непосредственно из утилиты SQL*Plus, рассмотрим использованные операторы, а затем — таблицу итоговых результатов:

Глава set autotrace traceonly statistics insert into t values (1, user, sysdate);

insert select from where insert select from where / into t object_id, object_name, created all_objects rownum <= 10;

into t object_id, object_name, created all_objects rownum <= declare l_redo_size number;

l_cnt number := 0;

begin select value into l_redo_size from redo_size;

for x in (select * from all_objects where rownum <= 200) loop insert into t values (x.object_id, x.object_name, x.created);

l_cnt := l_cnt+l;

end loop;

select value-l_redo_size into l_redo_size from redo_size;

dbms_output.put_line('redo size = ' || l_redo_size || ' rows = ' || l_cnt);

end;

/ Этот фрагмент кода выполняет описанные операторы вставки — 1, 10, 200 строк за раз, затем — 200 отдельных операторов INSERT. Выполним изменения:

update t set y=lower(y) where rownum = 1;

update t set y=lower(y) where rownum <= 10;

update t set y=lower(y) where rownum <= 200;

declare l_redo_size number;

l_cnt number := 0;

begin select value into l_redo_size from redo_size;

for x in (select rowid r from t where rownum <= 200) loop update t set y=lower(y) where rowid = x.r;

l_cnt := l_cnt+l;

end loop;

select value-l_redo_size into l_redo_size from redo_size;

dbms_output.put_line('redo size = ' || l_redo_size || ' rows = ' | | l_cnt);

end;

/ Повторное выполнение и откат и, наконец, удаления:

delete from t where rownum = 1;

delete from t where rownum <= 10;

delete from t where rownum <= 200;

declare l_redo_size number;

l_cnt number := 0;

begin select value into l_redo_size from redo_size;

for x in (select rowid r from t) loop delete from t where rowid = x.r;

l_cnt := l_cnt+l;

end loop;

select value-l_redo_size into l_redo_size from redo_size;

dbms_output.put_line('redo size = ' || l_redo_size || ' rows = ' || l_cnt);

end;

Вот результаты эксперимента: Операция Количество затронутых строк 1 10 200 200 1 10 200 200 1 10 200 200 Общий объем данных повторного выполнения 2679 22260 442784 464224 4228 42520 849600 849700 2236 23688 469152 469212 В среднем для строки Вставка одной строки Вставка 10 строк с помощью одного оператора Вставка 200 строк с помощью одного оператора Вставка 200 строк по одной Изменение одной строки Изменение 10 строк с помощью одного оператора Изменение 200 строк с помощью одного оператора Изменение 200 строк по одной Удаление одной строки Удаление 10 строк с помощью одного оператора Удаление 200 строк с помощью одного оператора Удаление 200 строк по одной 2679 2226 2213 2321 4228 4252 4248 4248 2236 2369 2345 Глава Интересно отметить, что при изменении 200 строк с помощью одного оператора и по одной генерируется одинаковый объем данных повторного выполнения. Это верно и для операторов DELETE: один или 200 операторов — результат одинаков. Операторы вставки ведут себя немного иначе. При вставке строк по одной генерируется немого больше данных повторного выполнения, что вполне логично, если учесть, что при вставке по одной строке данные в блоках организуются немного не так, как при множественной вставке (при этом необходимо выполнить немного больше работы). Как видите, объем генерируемых данных повторного выполнения зависит от объема изменяемых данных. При вставке строк размером 2000 байт генерируется немногим более 2000 байт для каждой строки. При выполнении оператора UPDATE объем генерируемых данных повторного выполнения удваивается (если помните, в журнал записываются данные и изменения в сегменте отката, так что это вполне логично). Оператор DELETE генерирует данные почти такого же объема, что и оператор INSERT. В сегменте отката записана вся строка, и это отражается в журнале, но записываются еще и изменение в блоке, что и объясняет небольшие расхождения. Поэтому, если известен объем изменяемых данных и то, как они будут изменяться, определить объем данных повторного выполнения легко. Этот пример не показывает, что построчная обработка так же эффективна, как и обработка множества строк. Он показывает только, что при этом генерируется одинаковый объем данных повторного выполнения. В предыдущих главах было продемонстрировано, что процедурная обработка строк никогда не бывает настолько эффективной, как обработка множеств. Кроме того, если выполнять в цикле операторы COMMIT, как делают многие исходя из ошибочного предположения, что этим экономят ресурсы, проблема только усложняется. Теперь, зная, как оценить объем генерируемых данных повторного выполнения, можно четко показать последствия этой неудачной идеи. Используем ту же схему, что и в предыдущем примере, и посмотрим, что произойдет при выполнении оператора COMMIT в цикле:

tkyte@TKYTE816> declare 2 l_redo_size number;

3 l_cnt number := 200;

4 procedure report 5 is 6 begin 7 select value-l_redo_size into l_redo_size from redo_size;

8 dbms_output.put_line('redo size = ' || l_redo_size || 9 ' rows = ' || l_cnt || ' ' || 10 to_char(l_redo_size/l_cnt, '99,999.9'} || 11 ' bytes/row');

12 end;

13 begin 14 select value into l_redo_size from redo_size;

15 fox x in (select object_id, object_name, created 16 from all_objects 17 where rownum <= l_cnt) 18 loop 19 insert into t values Повторное выполнение и откат 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 redo redo redo (x.object_id, x.object_name, x.created);

commit;

end loop;

report;

select value into l_redo_size from redo_size;

for x in (select rowid rid from t) loop update t set у = lower (у) where rowid = x.rid;

commit;

end loop;

report;

select value into l_redo_size from redo_size;

for x in (select rowid rid from t) loop delete from t where rowid = x.rid;

commit;

end loop;

report;

end;

/ size = 530396 rows = 200 2,652.0 bytes/row size = 956660 rows = 200 4,783.3 bytes/row size = 537132 rows = 200 2,685.7 bytes/row PL/SQL procedure successfully completed.

Pages:     | 1 |   ...   | 2 | 3 || 5 | 6 |   ...   | 24 |



© 2011 www.dissers.ru - «Бесплатная электронная библиотека»

Материалы этого сайта размещены для ознакомления, все права принадлежат их авторам.
Если Вы не согласны с тем, что Ваш материал размещён на этом сайте, пожалуйста, напишите нам, мы в течении 1-2 рабочих дней удалим его.