Обратная связь
|
По умолчанию установлен режим WAIT
Вы, вероятно, заметили, что в сообщении об ошибке конфликта блокировки фигурирует слово "deadlock", однако это слово выбрано не совсем удачно. В переводе с английского оно означает "мертвая блокировка", или "взаимоблокировка". В нашем случае, несмотря на грозное сообщение, никаких взаимоблокировок не возникает. Что же такое взаимоблокировка на самом деле и когда она может возникнуть?
Взаимоблокировка
Взаимоблокировка - классическая проблема при синхронизации доступа к ресурсу, при котором принципиально невозможна дальнейшая работа конкурирующих транзакций. Для иллюстрации рассмотрим две транзакции Т1 и Т2 и два ресурса - А и В; в контексте разговора о базах данных ресурсами могут быть, например, записи в некоторой таблице. Допустим, выполняется такая последовательность действий:
- Транзакция Т1 блокирует ресурс А, после чего благополучно работает с ним.
- Транзакция Т2 блокирует ресурс В, после чего также с ним работает.
- Транзакция Т1 желает поработать с ресурсом В, для чего она пытается установить на него блокировку. Так как в это время ресурс В уже занят транзакцией В, транзакция Т1 входит в состояние ожидания.
- Транзакция Т2 желает поработать с ресурсом А, пытается выполнить его блокирование и также переходит в состояние ожидания.
В результате мы имеем печальную ситуацию: транзакции не имеют никакого шанса продолжить свое выполнение из-за того, что намертво блокируют друг друга. Любой сервер баз данных должен быть способен выходить из этой ситуации по возможности достойно, и InterBase не исключение. Проблема решается выбором одной из транзакций в качестве жертвы и ее откате, при этом другая транзакция получает возможность выполниться до конца. Алгоритм распознавания ситуации взаимоблокировки в InterBase не запускается сразу при возникновении конфликта, что сделано из соображений производительности. Вместо этого выдерживается определенный интервал времени, задаваемый параметром DEADLOCK_TIMEOUT в конфигурационном файле InterBase ibconfig, только после этого и производится сканирование таблицы блокировок на предмет взаимного блокирования. В реальной практике программирования баз данных взаимоблокировки возникают крайне редко, поэтому не стоит считать, что вам так повезло, увидев слово "deadlock" в сообщении об ошибке. Скорее всего это всего лишь конфликт обновления. Теперь давайте рассмотрим параметры, которые влияют на уровни изоляции.
Установка уровней изоляции
Итак, как было упомянуто выше, уровень изоляции транзакции определяет, какие изменения, сделанные в других транзакциях, может видеть данная транзакция. Как было сказано в разделе "Уровни изоляции", в InterBase есть 3 основных уровня изоляции. Теоретически существует также четвертый уровень изоляции, так называемое DIRTY READ - "грязное чтение". Транзакции с уровнем изоляции DIRTY READ могут читать неподтвержденные данные в других транзакциях. В InterBase пользователю нельзя запускать транзакции с таким уровнем изоляции, хотя теоретически многоверсионная архитектура могла бы обеспечить такой уровень изоляции. Давайте перейдем к реально существующим в InterBase уровням изоляции. Сначала рассмотрим уровень Read Committed, задающийся константой read_committed. Транзакция, запущенная с таким уровнем изоляции, может читать изменения, произведенные из параллельно выполняющихся транзакций. Этот уровень изоляции часто используется для получения самого "свежего" состояния базы данных. Как видно из таблицы 1.4, существуют две разновидности этого уровня изоляции: read_committed rec_version и read_committed no_rec_version. По умолчанию используется вариант с параметром rec_version. Это означает, что при чтении какой-либо записи просто считывается последняя подтвержденная версия , записи. Вариант с no_rec_version более сложен для объяснения. Вообще говоря, суть использования уровня read_committed с опцией no_rec_version сводится к тому, "что транзакция будет не только пытаться считать самую последнюю подтвержденную версию записи, но и требовать, чтобы не было более свежей неподтвержденной версии. При чтении записи в такой транзакции производится проверка, не существует ли у этой записи неподтвержденной версии. Если существует, то происходит следующее (в зависимости оттого, какой режим блокировки выбран):
- Если wait, то наша транзакция ждет, пока не завершится транзакция, в которой создана неподтвержденная запись. И если она подтвердилась или отменилась, то считывается последняя подтвержденная версия.
- Если блокировка nowait, то немедленно возникает ошибка "Deadlock".
Очевидно, что уровень изоляции read_committed no_rec_version может привести к множеству конфликтов, и использовать его следует с большой осторожностью. Уровень изоляции SNAPSHOT задается параметром concurrency. Можно сказать, что SNAPSHOT - самый "родной" режим InterBase, при котором преимущества версионности проявляются наиболее полно. При его использовании транзакция делает "снимок" маски транзакций в базе данных на момент запуска, и поэтому, пока она длится, видит те же самые данные, которые существовали на момент ее запуска. Никакие изменения, которые делаются параллельно выполняющимися транзакциями, ей не видны. Ей видны только свои изменения. При попытке в этой транзакции изменить данные, измененные другими транзакциями уже после ее запуска (имеются в виду как уже подтвержденные, так и еще неподтвержденные данные), возникает конфликт. Пока выполняется транзакция с уровнем изоляции concurrency, удерживаются все версии записей на момент ее запуска, так как конкурирующие транзакции видят, что SNAPSHOT активен и не имеют права собрать версии записей, так как они могут (гипотетически) понадобится нашему SNAPSHOT. Обычно SNAPSHOT применяется либо для длительных по времени запросов (отчетов), либо для организации блокирования записей, чтобы предотвратить их одновременное редактирование/удаление другими транзакциями. Уровень изоляции SNAPSHOT TABLE STABILITY задается параметром consistency. Этот уровень изоляции аналогичен уровню SNAPSHOT, но дополнительно блокирует таблицу на запись. Суть идеи проста - если транзакция с уровнем изоляции consistency проводит изменения на какой-либо таблице, то транзакции с уровнями изоляции read_committed и concurrency могут только читать эту таблицу, а транзакции с таким же уровнем изоляции (т. е. consistency) не смогут даже читать. Очевидно, что использование этого уровня изоляции позволяет организовать последовательные (сериализуемые) обновления таблицы. Обычно такой уровень изоляции используется только для коротких обновляющих транзакций. Транзакция запускается, проводит очень короткое по времени изменение и сразу завершается Другие транзакции в зависимости от режима блокировки wait или nowait либо ждут своей очереди, либо возбуждают исключение.
Как использовать транзакции - с этим вопросом часто сталкиваются начинающие разработчики. Конечно, для каждой конкретной задачи нужно решать вопрос индивидуально. Обычно все запросы к базе данных подразделяются на группы - запросы на чтение самого "свежего" состояния базы данных, запросы на текущие изменения, запросы на чтение справочных таблиц, запросы на чтение данных для построения отчета и т. д. Для каждой группы запросов обычно устанавливается своя транзакция (или группа транзакций) с набором параметров, нужных для выполнения задачи. Рассмотрим типичное приложение базы данных, с помощью которого пользователь желает читать и изменять данные. В приложении имеется сетка (dbGrid в Delphi/C++Builder), в которой пользователь просматривает текущее содержание какой-то таблицы. Сетка содержит lookup-поля, которые заполняются значениями из справочников. Когда пользователь находит запись, которую нужно изменить (или просто желает добавить запись в таблицу), то он нажимает кнопку добавления/редактирования и в появившемся диалоге заполняет/изменяет поля записи и затем сохраняет/отменяет редактирование. Как же настроить транзакции для такого приложения? Для запроса SELECT. ., который читает данные в сетку, следует использовать транзакцию с доступом "только для чтения" с уровнем изоляции READ COMMITED, чтобы получить самые "свежие" данные из таблицы, как только они будут обновлены/добавлены (не надо забывать о том, что наше приложение многопользовательское и одновременно могут работать несколько приложений). Примерный набор параметров такой:
read read_committed rec_version nowait
При этом обеспечивается чтение всех подтвержденных другими транзакциями записей, причем без конфликтов с параллельно работающими пишущими и читающими транзакциями. Такую транзакцию можно длительное время держать открытой - сервер не нагружается версиями записей. Для запроса на изменение/добавление данных можно использовать транзакцию с уровнем изоляции concurrency. Запрос на обновление в этом случае должен быть очень коротким: пользователь заполняет необходимые поля, запускается транзакция, делается попытка выполнить запрос, и затем, если не возник н> конфликта на запись с другой транзакцией, подтверждение нашей транзакции или откат, если был конфликт (на уровне клиентского приложения конфлнмы проявляются в виде исключений, которые удобно отлавливать с помощью коп струкций try.. .except или try.. .catch) Параметры такой транзакции будут следующими:
write concurrency nowait
Такой набор параметров позволит нам сразу (nowait) выявить то, что запись редактируется/изменяется другим пользователем (возникнет ошибка), а также предотвратить попытки других пользователей начать изменение записей, трансформированных нашей транзакцией (у претендента возникнет ошибка "update conflict"). Надо отметить, что перед редактированием нужно перечитать запись, потому что она могла быть изменена, а в кеше сетки может все еще находиться старая версия Для запросов, которые применяются для построения отчетов, однозначно нужно использовать транзакцию с режимом доступа "только для чтения" и с уровнем изоляции concurrency:
read concurrency nowait
Такая транзакция будет возвращать строго те данные, что существовали на момент ее запуска, - это очень важная особенность для отчетов, которые строятся за несколько проходов по базе данных. Для запросов на чтение справочных данных можно использовать транзакцию, аналогичную запросу SELECT для выборки данных в сетку.
|
|