Что не входит в принципы acid
Руководство для начинающих: ACID и транзакции БД
Введение
Транзакции повсеместно применяются в современных enterprise системах, обеспечивая целостность данных даже в многопоточных средах. Так давайте начнем сначала с опеределения термина и контекста, в которым обычно применяются транзакции.
Транзакции — это группа операций на чтение/запись, выполняющихся только если все операции из группы успешно выполнены.
По сути транзакции характеризуются следующими четырьмя свойствами (также известными как ACID):
В реляционной БД каждое SQL выражение должно исполняться в пределах транзакции. Без явного определения границ транзакции БД будет использовать неявную транзакцию, которая покрывает каждое SQL выражение. Неявная транзакция начинается до того как выражение запустится и заканчивается (коммит или откат) после того как выражение выполнится.
Режим неявной транзакции известен так же как режим autocommit.
Для enterprise систем режим автокоммита следует избегать из-за серьезных проблем с производительностью, и данный режим не позволит включать группу множественных DML в единую атомарную «единицу работы».
Это очень важно понимать и далее мы обсудим каждое из свойств.
Атомарность
Атомарность позволяет объединить единые и независимые операции в «единицу работы», которая работает по принципу «все-или-ничего», успешно выполняющаяся только если все содержащиеся операции успешно выполнятся.
Транзакция может инкапсулировать изменение состояний. Транзакция должна всегда оставлять систему в консистентном состоянии, независимо от того сколько параллельных транзакций выполняются в любой промежуток времени.
Консистентность (согласованность)
Консистентность означает что все требования уникальности были соблюдены для каждой совершенной транзакции. Это подразумевает, что требования по всем ключам (primary и foreign key), типам данных, триггерам успешно пройдены и не было найдено нарушений требования уникальности.
Изоляция
Во время выполнения транзакции параллельные транзакции не должны оказывать влияние на её результат. Изоляция дает нам преимущество сокрытия нефинальных изменений состояния от внешнего мира, и так же неуспешные транзакции не должны никогда порушить состояние системы. Изоляция достигается через управление параллельным выполнением операций используя пессимистический или оптимистический механизм блокировки.
Долговечность
Успешная транзакция должна всегда изменять состояние системы и прежде чем закончить ее изменения состояния сохраняются в лог транзакций. Если Ваша система внезапно выключится или возникнет перебой с электричеством, тогда все незавершенные транзакции можно воспроизвести.
Для систем сообщений, как JMS, транзакции не являются обязательными. Именно по этой причине в спецификации есть режимы подтверждения вне транзакций.
Операции с файловой системой обычно не контролируются, но если бизнес-требования требуют транзакций для операций с файлами, Вы можете использовать инструмент, такой как XADisk.
В то время как для файловых систем и систем передачи сообщений использование транзакций опционально, то для БД контроль за транзакциями обязателен.
Уровни изолированности
Хотя некоторые СУБД предлагают MVCC, обычно управление параллельным выполнением операций достигается через блокировку. Но, как мы знаем, блокировка увеличивает долю сериализации выполняемого кода, влияя на параллелизацию.
Стандарты SQL определяют 4 уровня изолированности:
Все уровни, кроме SERIALIZABLE уровня подвержены аномалии данных (феномен), которые могут произойти согласно следующей схеме:
Isolation Level | Dirty read | Non-repeatable read | Phantom read |
READ_UNCOMMITTED — чтение незавершенных транзакций | да | да | да |
READ_COMMITTED — чтение завершенных транзакций | нет | да | да |
REPEATABLE_READ — повторяемое чтение | нет | нет | да |
SERIALIZABLE — последовательное чтение | нет | нет | нет |
Феномены
Давайте обсудим каждую из аномалий.
Dirty Read («Грязное» чтение)
«Грязное» чтение происходит когда транзакция может прочитать незаконченные изменения некоторой другой выполняемой транзакции. Явление случается из-за того что нет блокировки предотвращающей это. На изображении сверху можно увидеть что вторая транзакция использует неконсистентное значение первой транзакции, которая впоследствии подверглась «откату». Так как данный эффект возможен только при минимальном уровне изоляции, а по умолчанию используется более высокий уровень изоляции (READ COMMITTED), то в скрипте чтения данных уровень изоляции будет явно установлен как READ UNCOMMITTED. Если вернуть уровень изоляции по умолчанию (READ COMMITTED) для транзакции 2, то поведение поменяется.
Non-repeatable read (Неповторяющееся чтение)
Неповторяющееся чтение проявляется когда повторные чтения в рамках одной транзакции дают разные результаты из-за параллельных транзакций, которые обновили запись, которую мы читаем. Это нежелательно, так как мы в конечном итоге используем устаревшие данные. Это предотвращается размещением разделяемой блокировки (блокировка на чтение) на чтение записи на все протяжении выполнения текущей транзакции.
Транзакция 1 | Транзакция 2 | |
SET TRANSACTION ISOLATION LEVEL READ COMMITTED —SET TRANSACTION ISOLATION LEVEL REPEATABLE READ BEGIN TRAN;SELECT Value FROM Table1 WHERE DELAY ’00:00:10′;SELECT Value FROM Table1 WHERE > UPDATE Table1 COMMIT TRAN; | ||
Результат для READ COMMITTED | Value = 1 Value = 42 | Мгновенное выполнение |
Результат для REPEATABLE READ | Value = 1 Value = 1 | Ожидание завершения транзакции 1 |
Phantom read (Фантомное чтение)
Фантомное чтение происходит когда вторая транзакция вставляет запись, которая подходит по критериям предыдущей выборки первой транзакции. В результате получится, что одни и те же выборки в первой транзакции дают разные множества строк.
Следовательно, мы используем устравшие данные, который могут оказать негативное влияние на работу системы. Это предотвращается с помощью использования блокировки по диапазону или блокировки предикатами.
Транзакция 1 | Транзакция 2 | |
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ —SET TRANSACTION ISOLATION LEVEL SERIALIZABLE BEGIN TRAN; SELECT * FROM Table1 WAITFOR DELAY ’00:00:10′ SELECT * FROM Table1 INSERT INTO Table1 (Value) | ||
Результат для REPEATABLE READ: | — первый SELECT ID: 1; Value: 1 — второй SELECT ID: 1; Value: 1 ID: 2; Value: 100 | Мгновенное выполнение |
Результат для SERIALIZABLE: | — первый SELECT ID: 1; Value: 1 — второй SELECT ID: 1; Value: 1 | Ожидание завершения транзакции 1 |
Уровни изолированности по умолчанию
Даже если SQL стандарт обязывает использовать уровень изоляции SERIALIZABLE, большинство СУБД используют разный уровень по умолчанию.
СУБД | Уровень по умолчанию |
Oracle | READ_COMMITTED |
MySQL | REPEATABLE_READ |
Microsoft SQL Server | READ_COMMITTED |
PostgreSQL | READ_COMMITTED |
DB2 | CURSOR STABILITY |
Обычно, READ_COMMITED является правильным выбором, поскольку даже SERIALIZABLE не может защитить Вас от потери обновлений во время выполнения чтения/записей в разных транзакций и веб-реквестов. Вы должны принять во внимание эту информацию при принятии решения об уровне изоляции в требованиях enterprise систем.
ACID. Что под капотом у транзакции
От корректного функционирования базы данных (БД) может зависеть не только скорость, но и надежность приложения. Для глубокого погружения в задачи специалисту, как правило, нужно освоить работу с транзакциями – об этом и пойдет речь ниже. Рассмотрим виды и свойства транзакций, а также постараемся понять, как устроен этот механизм. Надеемся, что статья может быть полезна начинающим разработчикам и всем, кто хочет лучше разобраться в теме.
От автора: однажды у меня спросили, что такое транзакция. Я попробовал рассказать простыми словами, но у меня не получилось, хотя я часто использовал это понятие. Поэтому прежде, чем говорить о свойствах транзакций, постараемся дать определение, для начала своими словами.
Что такое транзакция (transaction)?
Транзакция — это некий набор связанных операций с базой данных.
В первом приближении это действительно так. Однако, пока определение неполное. Не хватает самого главного, а именно — этот набор операций должен представлять единую логическую систему с данными.
Например, давайте представим такую ситуацию: у каждого человека есть карта, с помощью которой он может совершать определенные действия, будь то онлайн-покупка, перевод денежных средств с карты на карту, оплата счетов и т.д. Какие операции происходят в базе данных при совершении перевода денежных средств с одного лицевого счета на другой? В этой ситуации необходимо выполнить два запроса к базе данных:
С первого лицевого счета происходит списание N-ой суммы денежных средств.
На второй лицевой счет идет зачисление этой же суммы.
В данном случае эти две операции связаны и составляют единую логическую систему работы с данными. Теперь можно дать полное определение транзакции.
Транзакция — это набор последовательных операций с базой данных, соединенных в одну логическую единицу.
Виды транзакций
Транзакции делят на два вида:
Неявные транзакции, которые предусмотрены на уровне базы данных. Например, БД задает отдельную инструкцию INSERT, UPDATE или DELETE как единицу транзакции.
Явные транзакции — их начало и конец явно обозначаются такими инструкциями, как BEGIN TRANSACTION, COMMIT или ROLLBACK.
В ORM Laravel при использовании фасада DB есть возможность явно указать транзакцию с помощью конструкции DB::transaction(). Если необходимо больше гибкости, можно обратиться к конструкциям DB::beginTransaction(), DB::rollBack(), DB::commit().
Свойства транзакции
Выделяют так называемые «магические» свойства транзакции, которые описываются аббревиатурой «ACID». Каждая буква аббревиатуры означает одно из свойств, о которых мы поговорим ниже.
Atomicity или атомарность (A)
Вернемся к предыдущему примеру с переводом денежных средств между двумя лицевыми счетами. Мы установили, что эти 2 операции, которые взаимодействуют с базой данных, являются операциями транзакции. А какие проблемы могут возникнуть, если мы просто выполним эти операции последовательно, отправив два запроса к БД?
Первый запрос выполнится успешно. С первого лицевого счета будет списана N-ая сумма денежных средств.
Однако, в случае той или иной технической ошибки во время выполнения второго запроса может случиться так, что денежные средства с одного лицевого счета уйдут, а на другой счет не поступят.
В этой ситуации речь идет о проблеме потери данных. В целях снижения этого риска транзакции обладают таким свойством, как атомарность (atomicity), неделимость: либо будут выполнены все действия транзакции, либо никакие.
Consistency или согласованность (C)
Согласованность означает, что если до выполнения транзакции данные в БД находятся в неком состоянии «good state»*, то они будут в этом же состоянии и после выполнения транзакции.
*Иными словами, выполняется некий набор условий. Примеры: в таблице countries не должно быть двух строк с названием страны «Российская Федерация»; возраст человека не может быть больше 150 лет.
На самом деле ни одна база данных не может гарантировать свойство согласованности. А всё потому, что поддержание консистентности — это прерогатива приложения, а не БД. База данных лишь предоставляет инструменты для выполнения данного свойства транзакции, например, уникальные ключи, внешние ключи и т.д.
Isolation или изоляция (I)
Переходим к самому интересному свойству — изоляции. Представим ситуацию, когда в определенный момент времени с системой работают несколько пользователей. Естественно, операции транзакции в БД выполняются параллельно, чтобы ускорить работу системы. Но у параллельной работы транзакций есть свои подводные камни:
Если операции транзакции взаимодействуют с разным набором непересекающихся данных, все работает корректно.
Но что будет, если две и более операций транзакции в один момент времени начнут работать с одним и тем же набором данных? Возникнет явление, называемое race condition (состояние гонки).
Выделяют несколько эффектов, связанных с этим явлением.
Эффект потерянного обновления возникает, когда несколько транзакций обновляют одни и те же данные, не учитывая изменений, сделанных другими транзакциями.
Представим, что у клиента банка есть счет, на котором находится 1000 денежных единиц. Транзакции А и В считывают данное значение из БД. Транзакция А должна увеличить данную сумму на 100 денежных единиц, а транзакция В — на 200. Транзакция А увеличивает сумму денежных единиц на счёте на 100 (итого 1100) и записывает значение в БД, транзакция В увеличивает сумму на 200 денежных единиц и записывает в БД (итого 1200). В результате на счете должно оказаться 1300, а по факту имеем 1200 денежных единиц.
Эффект грязного чтения возникает, когда транзакция считывает данные, которые еще не были зафиксированы.
Представим, что транзакция А переводит все деньги клиента на другой счет, но не фиксирует изменения. Транзакция В считывает изменения счёта А, получает 0 денежных единиц на счете и отказывает клиенту в выдаче наличных. Транзакция А прерывается и отменяет перевод между счетами.
Эффект неповторяемого чтения возникает, когда транзакция считывает дважды одну и ту же строку, но каждый раз получает разные результаты.
Например, по правилу согласованности клиент банка не может иметь отрицательный баланс на счёте. Транзакция А хочет уменьшить баланс счета клиента на 200 денежных единиц. Она проверяет текущее значение суммы на счёте — 500 денежных единиц. В это время транзакция В уменьшает сумму на счёте до 0 и фиксирует изменения. Если бы транзакция А повторно проверила сумму, то получила бы 0 денежных единиц, но на основе первоначальных данных она уже приняла решение уменьшить значение, и счет уходит в минус.
Эффект чтения фантомов возникает, когда набор данных соответствует условиям поиска, но изначально не отображается.
Например, правило согласованности запрещает иметь клиенту более 3 лицевых счетов. Для открытия нового счета транзакция А проверяет все счета клиента банка и в результате получает 2 счета. В этот момент транзакция B открывает еще один счет клиенту и фиксирует изменения (3 счета). Если бы транзакция А повторно проверила количество лицевых счетов клиента, то их оказалось бы 3, и по правилу согласованности открытие нового счета было бы невозможно.
Решение
Для устранения данных эффектов на уровне баз данных предусмотрены уровни изоляции, или transaction isolation levels, которые так или иначе реализованы во многих СУБД. Для примера рассмотрим движок InnoDB в СУБД MySQL:
Read uncommitted – это уровень изоляции, при котором каждая транзакция видит незафиксированные изменения другой транзакции. Справляется с эффектом потерянного обновления, но остаются остальные проблемы: эффекты грязного чтения, неповторяемого чтения, чтения фантомов.
Все запросы SELECT считывают данные в неблокирующей манере.
Блокирующее чтение (SELECT … FOR UPDATE, LOCK IN SHARE MODE), UPDATE и DELETE блокирует искомые индексные строки. Таким образом, возможна вставка данных в промежутки между индексами. Промежутки блокируются только при проверках на дублирующиеся и внешние ключи.
Read committed — это уровень изоляции, при котором параллельно исполняющиеся транзакции видят только зафиксированные изменения других транзакций. Справляется с эффектами потерянного обновления и грязного чтения, остаются эффекты неповторяемого чтения и чтения фантомов.
Согласованное чтение не накладывает блокировок, однако считывает данные из свежего снэпшота. В остальном ведёт себя так же, как и read uncommitted.
Repeatable read или snapshot isolation — это уровень изоляции, при котором транзакция не видит изменения данных, прочитанные ей ранее, однако способна прочитать новые данные, соответствующие условию поиска. Справляется с эффектами потерянного обновления, грязного чтения, неповторяемого чтения, остается эффект чтения фантомов.
Согласованное чтение не накладывает блокировок и считывает данные из снэпшота, который создается при первом чтении в транзакции. Таким образом, одинаковые запросы вернут одинаковый результат.
Блокировка для блокирующего чтения будет зависеть от типа условия:
если условие с диапазоном, например, WHERE (id > 7), то блокируется весь диапазон;
если уникальное, например, WHERE (id = 7), то блокируется одна индексная запись.
Кстати, в InnoDB именно уровень repeatable read используется по умолчанию.
Serializable — это уровень изоляции, при котором каждая транзакция выполняется так, как будто параллельных транзакций не существует. Справляется со всеми перечисленными выше эффектами.
Аналогично repeatable read, но есть интересный момент. Если выключен autocommit (а при явном старте транзакции START TRANSACTION он выключен по умолчанию), то все запросы SELECT превращаются в запросы SELECT … LOCK IN SHARE MODE.
SELECT … LOCK IN SHARE MODE – блокирует считываемые строки на запись.
SELECT … FOR UPDATE – блокирует считываемые строки на чтение.
Теперь, когда разобрались со всеми подводными камнями, сформулируем определение изоляции.
Изоляция — это свойство транзакции, которое позволяет скрывать изменения, внесенные одной операцией транзакции при возникновении явления race condition.
Durability или долговечность (D)
Долговечность означает, что если транзакция выполнена, и даже если в следующий момент произойдет сбой в системе, результат сохранится.
Если вы пользуетесь облачными хранилищами, такими как Amazon S3, то могли заметить, что разные тарифы обещают вам разное количество девяток durability. В контексте облака durability означает сохранность ваших данных и то, как они реплицируются. Чем больше копий ваших данных в разных точках мира, тем выше вероятность их не потерять из-за наводнения, землетрясения или нашествия инопланетян. В контексте «ACID» это обычно означает, что после фиксирования данные записываются в постоянное хранилище.
Вывод
Как мы рассмотрели выше, ошибки при проведении транзакций могут приводить к нежелательным последствиям в работе с системой. В статье мы осветили возможные риски при проведении транзакции и то, как ее “магические свойства” помогают справиться с каждой отдельной проблемой. Надеемся, что этот материал был вам полезен, и ждем ваших комментариев.
Разбираем ACID по буквам в NoSQL
Мотивация
«A» Атомарность
Атомарность гарантирует, что никакая транзакция не будет зафиксирована в системе частично. Будут либо выполнены все её подоперации, либо не выполнено ни одной
NoSQL системы обычно выбирают высокую производительность не в угоду транзакционной семантике, так как её соблюдение вносит дополнительные затраты на обработку. Многие системы всё же обеспечивают гарантию на уровне ключа или строки(Google BigTable) или предоставляют api для атомарных операций(Amazon DynamoDB), при которой только один поток может модифицировать запись, если вы, к примеру, хотите иметь счётчик посещений пользователя, распределённый по кластеру. Большинство систем придерживаются неблокирующих read-modify-write циклов. Цикл состоит из трёх этапов — прочитать значение, модифицировать, записать. Как видно, в мультипоточной среде есть много вещей, которые могут пойти не так, к примеру, что если кто-то изменит запись между фазами чтения и записи. Основной механизм разрешения таких конфликтов — использование алгоритма Compare and Swap, — если кто-то изменил запись в процессе цикла — мы должны понять, что запись поменялась и повторить цикл до тех пор, пока не установится наше значение, такой алгоритм выглядит более предпочтительным перед полностью блокирующим на запись механизмом. Количество таких циклов может быть очень большим, поэтому нам необходим некий timeout на операцию, по истечению которого операция будет отклонена.
«C» Консистентность
Транзакция достигающая своего нормального завершения и, тем самым, фиксирующая свои результаты, сохраняет согласованность базы данных. Учитывая специфику NoSQL к распределению информации по серверам — это означает — все ли реплики, содержащие копию данных всегда содержат одну и туже версию данных
В силу специфики, современные NoSQL обязаны выбирать высокую доступность и способность к горизонтальному масштабированию кластера, — получается что система не может обеспечить полную согласованность данных и идёт на некоторые допущения в определении понятия согласованности. Различают два подхода:
Строгая консистентность
Такие системы гарантируют, что реплики всегда способны прийти к соглашению об одной версии данных, возвращаемых пользователю. Некоторые реплики не будут содержать это значение, но когда система будет обрабатывать запрос значения по ключу, машина всегда сможет решить, какое значение вернуть — просто оно будет не всегда последним. Как это работает — к примеру у нас есть N реплик одного и того же ключа. Когда приходит запрос на обновление значения ключа, система не отдаст результат пользователю, пока W реплик не ответят, что они получили обновление. Когда пользователь запрашивает значение, система возвращает ответ пользователю ответ тогда, когда хотя бы R реплик вернули одно и то же значение. Тогда мы считаем систему строго консистентной, если соблюдается условие R+W>N. Выбор значений R и W влияет на то, сколько машин должны ответить перед тем, как ответ вернётся пользователю, обычно выбирают условие R+W=N+1 — минимально необходимое условие по обеспечению строгой консистентности.
Возможная консистентность
Некоторые системы(Voldemort, Cassandra, Riak) позволяют выбрать R и W при которых R+W UPDATE Users
SET login=’login’ AND password=’password’
WHERE key=’key’
то никакое конкурентное чтение не увидит частичное обновление данных(login изменился, а password — нет), причём это справедливо только на уровне строк, которые находятся в рамках одного column family и имеющие общий ключ. Это может соответствовать уровню изоляции транзакции read uncommitted, при котором разрешаются конфликты lost update. Но Cassandra не предоставляет механизма отката на уровне кластера, к примеру, возможна ситуация, когда login и password, будут сохранены на каком-то количестве нод, но недостаточном W для того, чтобы отдать пользователю верный результат, при этом пользователь вынужден разрешать этот конфликт сам. Механизм обеспечения изоляции заключается в том, что для каждой изменяемой записи создаётся невидимая, изолированная для клиентов версия, которая впоследствии автоматически заменяет старую версию через механизмы Compare and Swap, описанные выше.
«D» Надёжность
Независимо от проблем на нижних уровнях (к примеру, обесточивание системы или сбои в оборудовании) изменения, сделанные успешно завершённой транзакцией, должны остаться сохранёнными после возвращения системы в работу. Другими словами, если пользователь получил подтверждение от системы, что транзакция выполнена, он может быть уверен, что сделанные им изменения не будут отменены из-за какого-либо сбоя.
Самым прогнозируемым сценарием сбоя можно рассмотреть отключение питания или рестарт сервера. Полностью надёжная система в данном случае не должна вернуть ответ пользователю, пока не запишет все изменения из памяти на жёсткий диск. Запись на диск слишком долгая операция и многие NoSQL системы идут на компромиссы в угоду производительности.
Обеспечение надёжности в рамках одного сервера
Стандартный диск выдерживает 70-150 операций в секунду, что составляет пропускную способность до 150 Мб/c, ssd — 700 Мб/c, DDR — 6000 — 17000 Мб/c. Поэтому обеспечением надёжности в рамках одного сервера при обеспечении высокой производительности является сокращение числа записи со случайным доступом и увеличение последовательной записи. В идеале, система должна минимизировать число записей между вызовами fsync(синхронизации данных в памяти и на диске). Для этого применяются несколько техник.
Контролирование частоты fsync
Redis предлагает несколько способов для настройки того, когда вызывать fsync. Можно настроить, чтобы он вызывался после каждого изменения записи, — что является самым медленным и безопасным выбором. Для улучшения производительности можно вызывать сброс на диск каждые N секунд, в худшем случае вы потеряете данные за N последних секунд, что может быть вполне приемлимо для некоторых пользователей. Если надёжность совсем не критична, то можно отключить fsync и полагаться на то, что система сама в какой-то момент синхронизирует память с диском.
Увеличение последовательной записи через логирование
Для эффективного поиска данных NoSQL системы часто используют дополнительные структуры, например — B-деревья для построения индексов, — работа с ним вызывает множественные случайные доступы к диску. Для уменьшения этого некоторые системы (Cassandra, HBase, Riak) добавляют операции обновления в последовательно-записываемый файл, называемый redo log. Пока некоторые структуры записываются на диск достаточно редко, лог пишется часто. После падения недостающие записи можно восстановить с помощью лога.