Добавил:
СПбГУТ * ИКСС * Программная инженерия Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Тарасов С. В. СУБД для программиста. Базы данных изнутри

.pdf
Скачиваний:
80
Добавлен:
29.11.2021
Размер:
4.08 Mб
Скачать

CREATE OR REPLACE TRIGGER orders_bi_trigger BEFORE INSERT

ON orders FOR EACH ROW

WHEN (new.id_name is null) BEGIN

SELECT seq_orders.nextval INTO :new.id_name FROM DUAL; END orders_bi_trigger;

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

Например, пользователь вводит данные в таблицы, организованные по принципу «целое-части», «заказ-позиции заказа». Добавление строки в буфер позиций заказа требует указания идентификатора заказа в полях колонки внешнего ключа. Однако, если сам заказ ещё не создан, то его идентификатор неизвестен. Приходится использовать локальные значения, например отрицательные целые числа, чтобы отличить их от идентификаторовуже существующихобъектов, затем при сохранении меняя ихнареальные. Подобнойпроцедурыможноизбежать, еслизапроситьСУБД следующий номер в момент создания записи в буфере приложения.

Данный способ имеет некоторый недостаток по причине неизбежно образующихся «дыр» в последовательностях, поскольку не все буфера сохраняются, пользователь может просто отменить ввод.

Существуют модификации способов установки идентификаторами в приложении.

Методдиапазонов. Сервервыделяетприложениюдиапазонномеров, которое оно использует не обращаясь за новыми значениями.

Метод «верх-низ» (HiLo). Идентификатор разбивается на две части, старшие разряды раздаются приложениям сервером, младшие

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

211

Кроме последовательностей, некоторые СУБД, такие как DB2, MySQL, SQL Server и Sybase, поддерживают колонки автоинкрементных целочисленных типов.

CREATE TABLE cities

(

id_city int IDENTITY(1,1) PRIMARY KEY, city_name nvarchar (50),

)

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

INSERT INTO cities (city_name) VALUES ('Китеж');

SELECT @@identity; /* выводит полученное значение счетчика */

Большинство СУБД также позволяют работать с суррогатными ключами типа UUID (GUID). Основное преимущество данного метода в том, что значения представляют собой глобально уникальные идентификаторы, которые могут генерироваться как СУБД, так и приложением, без риска конфликтов их дублирования в распределённой БД. Однако, обеспечить монотонно возрастающую последовательность, рекомендуемую для таблиц-кластеров, в таком случае очень непросто. Если же осуществлять генерацию UUID только средствами СУБД, то имеется возможность упорядочивания. Например, функция NEWSEQUENTIALID() из арсенала SQL Server гарантирует монотонное увеличение всех возвращаемых значений.

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

Тем не менее, если встроенные реализации последовательностей не подходят для использования в приложении или по каким-то причинам требуется соблюсти транзакционность, существуют методы самостоятельной реализации. Перечислим некоторые из способов, примеры кода приведены для SQL Server.

212

Таблица счётчиков

В этом способе используется одна таблица на все счётчики. Получение нового значения в транзакции может привести к блокировке всей таблицы (подробнее см. главу «Транзакции, изоляция и блокировки»).

CREATE TABLE counters ( name varchar(128), value int,

CONSTRAINT PK_COUNTERS PRIMARY KEY (name) );

CREATE PROCEDURE GetNextValue( @counter varchar(128), @value int out)

AS BEGIN

UPDATE counters

SET @value = value = value + 1 WHERE counter = @counter

END

Если использовать переносимый ANSI SQL, то код получения нового значения изменится.

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ BEGIN TRANSACTION

UPDATE counters

SET value = value + 1

WHERE counter = 'table name'

SELECT value

FROM counters

WHERE counter = 'table name'

COMMIT

Использование identity

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

213

идентификаторов. Получение нового значения в транзакции не блокирует работу других процессов.

CREATE TABLE counter_myname ( value int identity(1, 1), foo bit,

CONSTRAINT PK_COUNTERS PRIMARY KEY (value)

)

GO

CREATE PROCEDURE GetNextMyNameValue(@value int out) AS BEGIN

SET NOCOUNT ON; IF @@trancount > 0

SAVE TRANSACTION tnx_GetNextMyNameValue ELSE

BEGIN TRANSACTION tnx_GetNextMyNameValue INSERT INTO counter_myname (foo) VALUES (0); SET @value = @@IDENTITY

ROLLBACK TRANSACTION tnx_GetNextMyNameValue END

Транзакции, изоляция и блокировки

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

Транзакционные файловые системы, такие как NTFS и ext4, давно сменили прежние, и теперь FAT32 можно встретить разве что на сменных картах памяти (flash cards) и USB-ключах. В стандартизированных объектных средах, например в CORBA, определена специальная служба транзакций (Transactional Service), позволяющая проводить атомарные операции. Абстрагирующие отслоя хранения ОРПимеютсвои интерфейсы работы с транзакциями, чаще всего, основанные на средствах целевой СУБД. Используя шаблон «Единица работы» (Unit of work) программист должен быть уверен: пакет изменений, отосланный веб-службе, будет принят или отменён целиком.

Любые попытки создать сколько-нибудь сложное приложение без использования транзакций обречены на провал, вызванный

214

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

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

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

При разработке программ на SQL и его процедурных расширениях транзакция включает в себя один и более операторы языка, оперирующих с данными. В однопользовательском режиме проблем использования механизма транзакций не возникает. Но стоит двум процессам одновременно приступить к чтению и изменению пересекающегося множества записей, как всё меняется. Могут ли пользователи видеть неподтвержденные изменения друг друга? Что происходит при попытке одновременной записи? Является ли повторно считанная запись идентичной первой?

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

Уровни SQL-92

Незавершённое (черновое) чтение (read uncommitted)

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

215

Процессы-читатели могут видеть и считывать изменённые данные незавершённой транзакции процесса-писателя.

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

Подтверждённое чтение (read committed)

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

Рис.54. Процессы на уровне подтверждённого чтения

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

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

216

Повторяемое чтение (repeatable read)

Уровень гарантирует, что повторное чтение данных вернёт одни и те же значения в течении всей транзакции.

Рис.55. Процессы на уровне повторяемого чтения

Однако неизменность чтения касается только существующих данных. Процессы-писатели могут вставлять новые записи, видимые после подтверждения читателям и имеющие статус «фантома». Повторное чтение новых записей также называется «фантомным».

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

Упорядоченное чтение (serializable)

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

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

217

Рис.56. Процессы на уровне упорядоченного чтения

Кроме стандартных уровней некоторые СУБД вводят дополнительно и свои собственные. Например, Microsoft SQL Server, начиная с версии 2005, поддерживает уровень мгновенного снимка (snapshot), являющегося по сути не самостоятельным уровнем, а механизмом разведения процессовчитателей и процессов-писателей. Процессы-читатели не ждут завершения транзакций писателей, а считывают версию данных по состоянию на момент начала своей транзакции.

Рис.57. Процессы при использовании мгновенного снимка

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

218

Блокировки

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

Однако, между блокировками системных ресурсов и записями таблиц базы данных имеются и некоторые важные отличия:

наименьшей единицей хранения является страница, а не запись;

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

Если процесс читает данные на уровне подтверждённого чтения, то блокировкуможносниматьсразупослеокончаниячтениястраницы. Аесли уровень — повторяемое или упорядоченное чтение? Тогда с большой вероятность придётся расставлять блокировки на все прочитанные страницы и поддерживать их до конца транзакции. При миллионах страниц для этого потребовались бы значительные системные ресурсы, прежде всего оперативная память и процессорное время.

Поэтому блокировки в СУБД имеют следующие принципиальные отличия:

им присуща разная степень грануляции;

обладают возможностью динамической эскалации и деэскалации грануляции;

классифицируются по типам, имеющих разную степень совместимости между собой.

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

219

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

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

Динамическая эскалация осуществляется СУБД автоматически по достижению определённого порога, определяемого статическими параметрами конфигурации и/или динамически в соответствии с наличествующими ресурсами и ценой операции.

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

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

разделяемые (совмещаемые) и монопольные (эксклюзивные);

уже наложенные и с намерением наложения;

чтение и модификацию.

Перечисленные признаки могут комбинироваться, например, «разделяемая блокировка чтения». Обозначим типы блокировок следующими символами.

Табл. 17. Обозначение некоторых типов блокировок

Тип блокировки Обозначение

Разделяемая

S

Монопольная

X

Намерение

I

Модификация

U

220