Oracle для профессионалов

         

Многоверсионность


Эта тема очень тесно связана с управлением одновременным доступом, поскольку создает основу для механизмов управления одновременным доступом в СУБД Oracle — Oracle использует модель многовариантной согласованности по чтению при одновременном доступе. В главе 3 мы более детально рассмотрим технические аспекты многоверсионности, но по сути это механизм, с помощью которого СУБД Oracle обеспечивает:

  • согласованность по чтению для запросов: запросы выдают согласованные результаты на момент начала их выполнения;
  • неблокируемые запросы: запросы не блокируются сеансами, в которых изменяются данные, как это бывает в других СУБД.
  • Это две очень важные концепции СУБД Oracle. Термин многоверсионность

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

    tkyte@TKYTE816> create table t 2 as 3 select * from all_users;

    Table created.

    tkyte@TKYTE816> variable x refcursor

    tkyte@TKYTE816> begin 2 open :x for select * from t; 3 end; 4 /

    PL/SQL procedure successfully completed.

    tkyte@TKYTE816> delete from t;

    18 rows deleted.

    tkyte@TKYTE816> commit;

    Commit complete.

    tkyte@TKYTE816> print x

    USERNAME USER_ID CREATED ------------------------------ ---------- --------- SYS 0 04-NOV-00 SYSTEM 5 04-NOV-00 DBSNMP 16 04-NOV-00 AURORA$ORB$UNAUTHENTICATED 24 04-NOV-00 ORDSYS 25 04-NOV-00 ORDPLUGINS 26 04-NOV-00 MDSYS 27 04-NOV-00 CTXSYS 30 04-NOV-00 ... DEMO 57 07-FEB-01



    18 rows selected.

    В этом примере мы создали тестовую таблицу T и заполнили ее данными из представления ALL_USERS. Мы открыли курсор для этой таблицы. Мы не выбирали данные с помощью этого курсора, просто открыли его.

    Помните, что при открытии курсора сервер Oracle не "отвечает" на запрос; он никуда не копирует данные при открытии курсора (представьте, сколько времени потребовало бы открытие курсора для таблицы с миллиардом строк в противном случае). Курсор просто открывается и дает результаты запроса по ходу обращения к данным. Другими словами, он будет читать данные из таблицы при извлечении их через курсор.


    В том же (или в другом) сеансе мы затем удаляем все данные из таблицы. Более того, мы даже фиксируем (COMMIT) это удаление. Строк больше нет — не так ли? На самом деле их можно извлечь с помощью курсора. Фактически, результирующее множество, возвращаемое командой OPEN, было предопределено в момент открытия курсора. Мы не прочитали при открытии курсора ни одного блока данных таблицы, но результат оказался жестко зафиксированным. Мы не сможем узнать этот результат, пока не извлечем данные, но с точки зрения нашего курсора результат этот неизменен. Дело не в том, что СУБД Oracle скопировала все эти данные в другое место при открытии курсора; данные сохранил оператор delete, поместив их в область данных под названием сегмент отката.

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

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

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

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

    В реальной таблице счетов будут сотни тысяч строк, но для простоты мы будем рассматривать таблицу всего с четырьмя строками (более детально мы рассмотрим этот пример в главе 3):

    Строка Номер счета Баланс счета
    1 123 500,00 $
    2 234 250,00 $
    3 345 400,00 $
    4 456 100,00 $
    Требуется создать отчет, который в конце банковского дня позволяет определить количество денег в банке. Это делается с помощью предельно простого запроса:



    select sum(account_balance) from accounts;

    Конечно, в данном примере ответ очевиден - 1250 $. Однако что произойдет, если мы прочитаем строку 1, а при считывании строк 2 и 3 с одного из банкоматов будет выполнена транзакция, переводящая 400 $ со счета 123 на счет 456? Наш запрос прочтет 500 $ в строке 4 и выдаст результат 1650 $, не так ли? Конечно, этого надо избежать, так как подобный результат ошибочен — никогда такого баланса по счетам в базе данных не было. Нужно понять, как СУБД Oracle избегает подобных ситуаций и чем отличаются используемые при этом методы от используемых во всех остальных СУБД.

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

    Как уже было сказано, вы не сможете в полном объеме использовать преимущества СУБД Oracle, если не понимаете концепцию многоверсионности. В СУБД Oracle многоверсионность используется для получения результатов, соответствующих моменту начала выполнения запроса, при этом не блокируется ни единой строки (пока транзакция по переводу денег изменяет строки 1 и 4, они будут заблокированы от других изменений, но не от чтения, выполняемого, например, нашим запросом SELECT SUM...). Фактически в СУБД Oracle нет "разделяемых блокировок чтения", типичных для других СУБД, — они в ней просто не нужны. Все устранимые препятствия для одновременного доступа были устранены.



    Итак, как же СУБД Oracle получает корректный, согласованный результат (1250 $) при чтении, не блокируя данных, другими словами, не мешая одновременному доступу? Секрет — в механизме выполнения транзакций, используемом в СУБД Oracle. При любом изменении данных Oracle создает записи в двух разных местах. Одна запись попадает в журнал повторного выполнения, где Oracle хранит информацию, достаточную для повторного выполнения, или "наката", транзакции. Для оператора вставки это будет вставляемая строка. Для оператора удаления это будет запрос на удаление строки в слоте X блока Y файла Z. И так далее. Другая запись — это запись отмены, помещаемая в сегмент отката. Если транзакция завершается неудачно и должна быть отменена, СУБД Oracle будет читать "предварительный" образ из сегмента отката, восстанавливая необходимые данные. Помимо отмены транзакций, СУБД Oracle использует сегменты отката для отмены изменений в блоках при их чтении, то есть для восстановления данных блока на момент начала выполнения запроса. Это позволяет читать данные несмотря на блокировку и получать корректные, согласованные результаты, не блокируя данные.

    Итак, в нашем примере Oracle получает результат следующим образом:

    Время Запрос Транзакция по переводу со счета на счет
    T1 Читает строку 1, sum получает значение 500 $  
    T2   Изменяет строку 1, устанавливает исключительную блокировку на строку 1, предотвращая другие изменения. В строке 1 теперь хранится значение 100 $
    T3 Читает строку 2, sum получает значение 750 $  
    T4 Читает строку 3, sum получает значение 1150 $  
    T5   Изменяет строку 4, устанавливает исключительную блокировку на строку 4, предотвращая другие изменения (но не чтение). В строке 4 теперь хранится значение 500 $.
    T6 Читает строку 4, определяет, что она была изменена. Выполняется откат блока до того состояния, которое он имел в момент времени T1. Запрос затем прочитает значение 100 $ из этого блока  
    T7   Транзакция фиксируется
    T8 Выдает 1250 $ в качестве результата суммирования  
    <


    В момент времени T6 СУБД Oracle фактически " читает поверх" блокировки, установленной транзакцией на строке 4. Именно так реализуется неблокируемое чтение: СУБД Oracle просто проверяет, изменились ли данные, игнорируя тот факт, что они в настоящий момент заблокированы (т.е. определенно изменены). Она извлечет старое значение из сегмента отката и перейдет к следующему блоку данных.

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

    Это согласованное по чтению представление данных всегда выполняется на уровне оператора SQL, — результаты выполнения любого оператора SQL всегда согласованы на момент его начала. Именно это свойство позволяет получать предсказуемый набор данных в результате, например, следующих вставок:

    for x in (select * from t) loop insert into t values (x.username, x.user_id, x.created); end loop;

    Результат выполнения оператора SELECT * FROM T предопределен в момент начала выполнения запроса. Оператор SELECT не будет "видеть" новых данных, генерируемых операторами INSERT. Представьте себе, что было бы в противном случае: оператор превратился бы в бесконечный цикл. Если бы по мере генерации оператором INSERT дополнительных строк в таблице T, оператор SELECT мог "видеть" эти вставляемые строки, представленный выше фрагмент кода создал бы неизвестное количество строк. Если бы в таблице T первоначально было 10 строк, в результате могло бы получиться 20, 21, 23 или бесконечное количество строк. Точно предсказать результат было бы невозможно. Согласованность по чтению обеспечивается для всех операторов, так что операторы INSERT, вроде представленного ниже, тоже работают предсказуемо:

    insert into t select * from t;

    Оператор INSERT получит согласованное по чтению представление таблицы T — он не "увидит" строки, которые сам же только что вставил, и будет вставлять только строки, существовавшие на момент начала его выполнения. Во многих СУБД подобные рекурсивные операторы просто не разрешены, поскольку они не могут определить, сколько строк вообще будет вставлено.

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


    Содержание раздела