суббота, 5 октября 2013 г.

Вперед к основам: ACID

В этой статье я проиллюстрирую требования к транзакционной системе, известные как ACID, примерами работы с СУБД Oracle. Попутно будут рассмотрены и проиллюстрированы примерами уровни изоляции транзакций.

Транзакционная система - это система, удовлетворяющая требованиям ACID. СУБД с поддержкой транзакций относятся к таковым. Требования ACID следующие:

Atomicity - атомарность
Транзакция либо применяется целиком, либо отменяется целиком.
Consistency - согласованность
Система находится в согласованном состоянии до начала транзакции и после завершения транзакции.
Isolation - изолированность
Внутри транзакции не видны изменения, сделанные в рамках другой непримененной транзакции.
Durability - долговечность
Результат примененной транзакции доступен после краха и восстановления системы.

Для экспериментов мне понадобятся два пользователя, один будет делать изменения, другой - наблюдать за изменениями:

SQL> connect system@xe
Connected as system@xe

SQL> CREATE USER broker IDENTIFIED BY do;
User created
SQL> GRANT connect, resource TO broker;
Grant succeeded

SQL> CREATE USER beholder IDENTIFIED BY look;
User created
SQL> GRANT connect, resource TO beholder;
Grant succeeded

SQL> CREATE TABLE broker.property (
  2      owner VARCHAR2(30) NOT NULL,
  3      item VARCHAR2(30) NOT NULL);
Table created

SQL> INSERT INTO broker.property VALUES ('лукавый', 'деньги');
1 row inserted
SQL> INSERT INTO broker.property VALUES ('беспечный', 'душа');
1 row inserted

SQL> GRANT SELECT ON broker.property TO beholder;
Grant succeeded
SQL> CREATE SYNONYM beholder.property FOR broker.property;
Synonym created

Атомарность

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

Следующий пример демонстрирует проведение брокером сделки по обмену, в ходе которой два предмета должны поменять собственников (либо каждый должен остаться при своем):


SQL> connect broker/do@xe
Connected as broker@xe

SQL> UPDATE property SET item = 'душа' WHERE owner = 'лукавый';
1 row updated

SQL> connect beholder/look@xe
Connected as beholder@xe

SQL> SELECT * FROM property;
OWNER                ITEM
-------------------- --------------------
лукавый              деньги
беспечный            душа
-- broker
SQL> UPDATE property SET item = 'деньги' WHERE owner = 'беспечный';
1 row updated
-- beholder
SQL> SELECT * FROM property;
OWNER                ITEM
-------------------- --------------------
лукавый              деньги
беспечный            душа
-- broker
SQL> COMMIT;
Commit complete
-- beholder
SQL> SELECT * FROM property;
OWNER                ITEM
-------------------- --------------------
лукавый              душа
беспечный            деньги

Этот пример попутно демонстрирует изолированность изменений, выполняемых в рамках одной транзакции, от других транзакций. beholder не видит изменений, сделанных broker, до тех пор, пока broker не завершит транзакцию. Два изменения, сделанные broker последовательно в одной транзакции, становятся видны beholder одновременно после завершения транзакции broker - из-за изолированности транзакций.

Согласованность

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

-- broker
SQL> UPDATE property SET item = 'душа' WHERE owner = 'беспечный';
1 row updated

SQL> UPDATE property SET item = 'деньги' WHERE owner = 'лукавый';
1 row updated

SQL> SELECT * FROM property;
OWNER                ITEM
-------------------- --------------------
лукавый              деньги
беспечный            душа
-- beholder
SQL> SELECT * FROM property;
OWNER                ITEM
-------------------- --------------------
лукавый              душа
беспечный            деньги
-- broker
SQL> ROLLBACK;
Rollback complete

SQL> SELECT * FROM property;
OWNER                ITEM
-------------------- --------------------
лукавый              душа
беспечный            деньги
-- beholder
SQL> SELECT * FROM property;
OWNER                ITEM
-------------------- --------------------
лукавый              душа
беспечный            деньги

Здесь beholder даже не заметил попытки обмена.

Если бы в результате сбоя или злонамеренного вмешательства в БД сохранилось бы только одно изменение, то один из владельцев остался бы ни с чем, а другой - с двумя предметами. Транзакционная система по определению гарантирована от такого исхода.

К слову, ограничения целостности (integrity constraints) обеспечивают целостность данных на уровне команд SQL (если это не отложенные органичения). Охраняемая ими целостность данных и транзакционная согласованность - разные вещи, хотя и то и другое работает на качество данных и соблюдение бизнес-правил.

Изолированность

Транзакции изолированы друг от друга. Изоляция работает на логическую согласованность, скрывая от других транзакций временные состояния рассогласованности. В самом деле, для чего наблюдателю сделки видеть, что после первой команды UPDATE один предмет уже сменил владельца, а другой - пока нет? Это состояние кратковременно и сменится согласованным состоянием после выполнения еще одной команды UPDATE.

По-умолчанию, в Oracle установлен уровень изоляции транзакций, называемый READ COMMITED (читать примененные изменения). Работу этого уровня мы и видели в рассмотренных примерах.

Два других возможных уровня изоляции в Oracle, READ ONLY (только чтение) и SERIALIZABLE (последовательный), позволяют текущей транзакции видеть только изменения, примененные до начала этой транзакции. В рамках READ ONLY транзакции изменения не разрешены (получим ошибку), а в рамках SERIALIZABLE транзакции - разрешены.

-- beholder
SQL> SELECT * FROM property;
OWNER                ITEM
-------------------- --------------------
лукавый              душа
беспечный            деньги

SQL> SET TRANSACTION READ ONLY;
Transaction set
-- broker
SQL> UPDATE property SET item = 'душа' WHERE owner = 'беспечный';
1 row updated

SQL> UPDATE property SET item = 'деньги' WHERE owner = 'лукавый';
1 row updated

SQL> COMMIT;
Commit complete
-- beholder
SQL> SELECT * FROM property;
OWNER                ITEM
-------------------- --------------------
лукавый              душа
беспечный            деньги

beholder не видит примененные изменения! Завершим транзакцию тем или иным образом и повторно выполним запрос:

-- beholder
SQL> ROLLBACK;
Rollback complete

SQL> SELECT * FROM property;
OWNER                ITEM
-------------------- --------------------
лукавый              деньги
беспечный            душа

Теперь beholder видит изменения, сделанные broker. Попробуем уровень изоляции SERIALIZABLE:

-- beholder
SQL> SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
Transaction set
-- broker
SQL> UPDATE property SET item = 'деньги' WHERE owner = 'беспечный';
1 row updated

SQL> UPDATE property SET item = 'душа' WHERE owner = 'лукавый';
1 row updated

SQL> COMMIT;
Commit complete
-- beholder
SQL> SELECT * FROM property;
OWNER                ITEM
-------------------- --------------------
лукавый              деньги
беспечный            душа

SQL> COMMIT; -- теперь завершим транзакцию командой COMMIT
Commit complete

SQL> SELECT * FROM property;
OWNER                ITEM
-------------------- --------------------
лукавый              душа
беспечный            деньги

Заслуживает упоминания, что изолированные транзакции в Oracle не приводят к блокировкам, мешающим другим транзакциям изменять или читать данные. "Readers don't block writers, writers don't block readers" (Oracle). Это достигается благодаря реализации в СУБД Oracle многоверсионности данных за счет использования undo records (как это по-русски...)

В теории выделяют также самый низкий уровень транзакции, READ UNCOMMITED. На этом уровне непримененные изменения, сделанные в транзакции 1, видны из транзакции 2. Следовательно, транзакция 2 может наблюдать логически несогласованное состояние данных, а также изменения, которые могут быть отменены (rolled back) транзакцией 1, либо оказаться несохраненными в случае краха системы. В Oracle работа на таком уровне изоляции невозможна.

Долговечность

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

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

Может возникнуть вопрос: почему бы команде COMMIT не писать изменения сразу в файлы данных? Потому, что это будет очень неэффективно с точки зрения производительности. Информация об изменениях для журнала изменений достаточно компактна и записывается последовательно в файл журнала изменений. Применение же изменений к файлам данных означаает в общем случае запись многих блоков данных, физически расположенных в разных файлах: ведь изменение данных в таблице влечет изменение индексов (и сопровождается созданием undo records, размещаемых в файлах табличного простанства undo). Потому-то изменения данных выполняются в оперативной памяти, а запись их в файлы данных выполняется отдельным фоновым процессом по оптимальному алгоритму.

Я не буду демонстрировать аварийное завершение СУБД. В заключение, ликвидирую следы моих экспериментов:

SQL> connect system@xe
Connected as system@xe

SQL> DROP USER broker CASCADE;
User dropped

SQL> DROP USER beholder CASCADE;
User dropped

Ложка дегтя

Известный эксперт по Oracle Джонатан Льюис в своем блоге приводит пример, демонстрирующий, что СУБД Oracle не является ACID-системой. А именно, изменения, сделанные в транзакции 1, которая находится в процессе выполнения COMMIT, становятся видимы в транзакции 2 прежде, чем они записаны в журнал изменений на диске. Таким образом, нарушено требование изолированности: поведение системы в данном случае не соответствует уровню READ COMMITED и обманывает ожидания всех клиентов.

Это очень серьезно как для бизнеса, полагающегося на СУБД Oracle, так и для репутации Oracle.

Комментариев нет:

Отправить комментарий