Интернет Windows Android

Введение в транзакции в MySQL. Как оказалось, знают все, а понимают не все

Транзакция – это механизм, который позволяет интерпретировать множественные изменения в базе данных как единую операцию. Либо будут приняты все изменения, либо все они будут отвергнуты. Ни из какого другого сеанса невозможно получить доступ к таблице, пока есть открытая транзакция, в рамках которой выполняются какие-либо изменения в этой таблице. Если вы в своем сеансе попробуете сделать выборку данных сразу же после их изменения, все выполненные изменения будут доступны.

Такой механизм базы данных с поддержкой транзакций, как InnoDB или BDB, начинает транзакцию по команде start transaction. Завершается транзакция при подтверждении или отмене изменений. Завершить транзакцию можно двумя командами. Команда commit сохраняет все изменения в базе данных. Команда rollback отменяет все изменения.

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

CREATE TABLE sample_innodb (id int(11) NOT NULL auto_increment, name varchar(150) default NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO sample_innodb VALUES (1, "Александр"), (2, "Дмитрий"); start transaction; DELETE FROM sample_innodb WHERE id = 1; DELETE FROM sample_innodb WHERE id = 2; rollback;

Поскольку произошел откат транзакции, данные из таблицы не были удалены.

А если бы вместо rollback мы написали commit, то обе строки были бы удалены.

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

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

По долгу службы мне приходится иногда проводить собеседования на позицию "[старший|младший] разработчик python/django", «тимлид». К моему великому удивлению я обнаружил, что 9 из 10 соискателей, в резюме которых значатся слова " Mysql/Innodb/transactions/triggers/stored proc etc.", абсолютно ничего не могут рассказать о своем прошлом опыте работы с ними. Ни одного описания варианта использования, к сожалению, я так и не получил.

Далее по собеседованию я предлагал попробовать предложить вариант решения для следующей ситуации:

Допустим, мы являемся онлайн сервисом, который в свою очередь пользуется каким-то внешним платным API (активация услуги, платный контент, или что вашей душе угодно), то есть наш сервис сам платит деньги за пользование API. Пользователь в нашей системе создает запрос на активацию услуги, заполняет все поля и на последней странице жмет кнопку «Активировать услугу». То есть на момент отправки HTTP запроса мы имеем в нашей БД запись (запрос на активацию услуги). Каков наш алгоритм?- спрашиваю я и сам продолжаю:

Достаем из базы баланс пользователя;
- если баланса достаточно, то дергаем API;
- если всё хорошо, то списываем с баланса сумму за услугу, делаем UPDATE, коммитим, иначе откатываемся;
- отвечаем пользователю.

Вроде бы всё тривиально, но когда привожу первую и самую очевидную проблему в виде 10 конкурентных запросов (что все они в начале получат одинаковый баланс и начнут звонить в API), решения начинают предлагать самые изощренные, начиная от выполнения 5 селектов (стоит признаться, я ничего не понял в этом варианте), использования автоинкрементных счетчиков, внешних кешей, новых таблиц в бд, слипов и еще не пойми чего.

Как известно (а это знали все кандидаты!), innodb в mysql предоставляет транзакционный механизм и возможность построчной блокировки. Для того, чтобы применить этот самый построчный лок, достаточно добавить в конце SELECT-а выражение FOR UPDATE, например так:

SELECT * FROM requests WHERE id=5 FOR UPDATE

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

Также стоит заметить, что использование FOR UPDATE лучше делать с выключенным autocommit-ом, так как вне зависимости от того, что вы залочили, после первого апдейта лок снимется.

Вроде мелочь, вроде очевидно, но 9 из 10…

Upd
прежнее название «Транзакции в mysql», не раскрытое в статье было заменено на " Транзакции в mysql и SELECT FOR UPDATE"

ЗЫ
в статье не говорится о том, что АПИ нужно дергать в рамках транзакции и что делать в случае сбоя и как обрабатывать исключительные ситуации.

Транзакционный механизм поддерживают только InnoDB и BDB. Поэтому все таблицы с которыми вы хотите работать через транзакции следует переконвертировать в соответствующий тип. Можно .

  • По умолчанию MySQL работает в режиме autocommit. Это означает, что результаты выполнения любого SQL-оператора, изменяющего данные, будут сразу сохраняться.
    Режим autocommit можно отключить так: SET AUTOCOMMIT=0;
  • Если необходимо переключиться из режима AUTOCOMMIT только для выполнения одной последовательности команд, то для этого можно использовать команду BEGIN или START TRANSACTION (начиная с версии mysql 4.0.11.)
  • Пример SQL-запроса, фиксирующего количество выбранного покупаелем товара в таблице corrections и вносящего изменения в таблицу товаров goods:
    START TRANSACTION; INSERT INTO corrections SET id_goods=:id_goods, number=:number, id_orders=:id_orders ON DUPLICATE KEY UPDATE number = number + :number; UPDATE goods SET reserve = reserve + :number, available = available - :number WHERE id = :id_goods; COMMIT;
  • Транзакция завершается оператором COMMIT. Изменения сохраняются. В случае ошибки в одном из запросов, изменения не будут сохранены ни в одной таблице.
  • Если Вам требуется создать более сложный механизм фиксации изменений, используйте команды SAVEPOINT и ROLLBACK TO SAVEPOINT
  • Следующие операторы неявно завершают транзакцию (как если бы перед их выпол­нением был выдан COMMIT):
    • ALTER TABLE
    • DROP DATABASE
    • LOAD MASTER DATA
    • SET AUTOCOMMIT = 1
    • BEGIN
    • DROP INDEX
    • LOCK TABLES
    • START TRANSACTION
    • CREATE INDEX
    • DROP TABLE
    • RENAME TABLE
    • TRUNCATE TABLE
  • PHP PDO предлагает свои средства работы с транзакциями. Про них можно почитать .
  • Параллельные транзакции и уровни изоляции (совместный доступ)

    Представим, что во время выполнения 1-ой транзакции транзакции, другой пользователь создал вторую параллельную транзакцию и сделал запрос SELECT * FROM user после того, как в нашей транзакции был выполнен первый запрос «INSERT INTO user (id, nik) VALUES (1, ‘nikola’)».
    Что увидит пользователь второй транзакции?
    Сможет ли он увидеть вставленную запись даже тогда, когда результаты первой транзакции еще не зафиксировались (не произошел COMMIT)? Или он сможет увидеть изменения только после того, как результаты первой транзакции будут зафиксированы?

    Оказывается имеют место быть оба варианта. Все зависит от уровня изоляции транзакции.

    У транзакций есть 4 уровня изоляции:

    • 0 - Чтение неподтверждённых данных (грязное чтение) (Read Uncommitted, Dirty Read) - самый низкий уровень изоляции. При этом уровне возможно чтение незафиксированных изменений параллельных транзакций. Как раз в этом случае второй пользователь увидит вставленную запись из первой незафиксированной транзакции. Нет гарантии, что незафиксированная транзакция будет в любой момент откачена, поэтому такое чтение является потенциальным источником ошибок.
    • 1 - Чтение подтверждённых данных (Read Committed) - здесь возможно чтение данных только зафиксированных транзакций. Но на этом уровне существуют две проблемы. В этом режиме строки, которые участвуют в выборке в рамках транзакции, для других параллельных транзакций не блокируются, из этого вытекает проблема № 1:

      «Неповторяемое чтение» (non-repeatable read) - это ситуация, когда в рамках транзакции происходит несколько выборок (SELECT) по одним и тем же критериям, и между этими выборками совершается параллельная транзакция, которая изменяет данные, участвующие в этих выборках. Так как параллельная транзакция изменила данные, результат при следующей выборке по тем же критериям в первой транзакции будет другой.

      Проблема № 2 - «Фантомное чтение» - этот случай рассмотрен ниже.

    • 2 - Повторяемое чтение (Repeatable Read, Snapshot) - на этом уровне изоляции так же возможно чтение данных только зафиксированных транзакций. Так же на этом уровне отсутствует проблема «Неповторяемого чтения», то есть строки, которые участвуют в выборке в рамках транзакции, блокируются и не могут быть изменены другими параллельными транзакциями. Но таблицы целиком не блокируются. Из-за этого остается проблема «фантомного чтения». «Фантомное чтение» - это когда за время выполнения одной транзакции результат одних и тех же выборок может меняться по причине того, что блокируется не вся таблица, а только те строки, которые участвуют в выборке. Это означает, что параллельные транзакции могут вставлять строки в таблицу, в которой совершается выборка, поэтому два запроса SELECT * FROM table могут дать разный результат в разное время при вставке данных параллельными транзакциями.
    • 3 - Сериализуемый (Serializable) - сериализуемые транзакции. Самый надежный уровень изоляции транзакций, но и при этом самый медленный. На этом уровне вообще отсутствуют какие либо проблемы параллельных транзакций, но за это придется платить быстродействием системы, а быстродействие в большинстве случаев крайне важно.

    По умолчанию в MySQL установлен уровень изоляции № 2 (Repeatable Read). И, как я считаю, разработчики MySQL не зря сделали по умолчанию именно этот уровень, так как он наиболее удачный для большинства случаев. С первого раза может показаться, что самый лучший вариант № 3 - он самый надежный, но на практике вы можете испытать большие неудобства из-за очень медленной работы вашего приложения. Помните, что многое зависит не от того, насколько хорош уровень изоляции транзакций в БД, а от того, как спроектировано ваше приложение. При грамотном программировании, можно даже использовать самый низкий уровень изоляции транзакций - все зависит от особенностей структуры и грамотности разработки вашего приложения. Но ненужно стремиться к самому низкому уровню изоляции - нет, просто если вы используйте не самый защищенный режим, следует помнить о проблемах параллельных транзакций, в этом случае вы не растеряетесь и все сделайте правильно.

    SET TRANSACTION - этот оператор устанавливает уровень изоляции следующей транзакции, глобально либо только для текущего сеанса.

    SET TRANSACTION ISOLATION LEVEL { READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE }

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

    Вы можете также установить начальный глобальный уровень изоляции для сервера mysqld, запустив его с опцией -transaction-isolation

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

    Любая транзакция либо выполняется полностью, либо не выполняется вообще.

    В транзакционной модели есть два фундаментальных понятия: COMMIT и ROLLBACK. COMMIT означает фиксацию всех изменений в транзакции. ROLLBACK означает отмену (откат) изменений, произошедших в транзакции.

    При старте транзакции все последующие изменения сохраняются во временном хранилище. В случае выполнения COMMIT, все изменения, выполненные в рамках одной транзакции, сохранятся в физическую БД. В случае выполнения ROLLBACK произойдет откат и все изменения, выполненные в рамках этой транзакции, не сохранятся.

    В MySQL транзакции поддерживаются только таблицами innoDB. Таблицы MyISAM транзакции не поддерживают. В innoDB по умолчанию включен autocommit, это значит, что по умолчанию каждый запрос эквивалентен одной транзакции.

    Транзакция начинается со специального запроса «START TRANSACTION», либо «BEGIN». Чтобы закончить транзакцию, нужно либо зафиксировать изменения (запрос COMMIT), либо откатить их (запрос ROLLBACK).

    Пример с COMMIT:

    Start transaction; (также, можно написать BEGIN ;) …какие-то действий с БД (insert, update,delete…) commit; //Фиксация действий, запись их в физическую БД

    Пример с ROLLBACK:

    Set autocommit=0; //отключаем autocommit Start transaction; …какие-то действия с БД (insert, update,delete…) rollback; // отменяем серию действий, не производим запись в физическую БД

    В MySQL не существует механизма вложенных транзакций. Одно соединение с БД — одна транзакция. Новая транзакция в пределах одного соединения может начаться только после завершения предыдущей.

    Для некоторых операторов нельзя выполнить откат с помощью ROLLBACK. Это операторы языка определения данных (Data Definition Language — DDL). Сюда входят запросы CREATE, ALTER, DROP, TRUNCATE, COMMENT, RENAME.

    Следующие операторы неявно завершают транзакцию (как если бы перед их выпол­нением был выдан COMMIT):

    • ALTER TABLE
    • DROP DATABASE
    • LOAD MASTER DATA
    • SET AUTOCOMMIT = 1
    • BEGIN
    • DROP INDEX
    • LOCK TABLES
    • START TRANSACTION
    • CREATE INDEX
    • DROP TABLE
    • RENAME TABLE
    • TRUNCATE TABLE

    Обратите внимание, что в случае SQL ошибки, транзакция сама по себе не откатится. Обычно ошибки обрабатываются уже с помощью sql wrapper’ов в самом приложении, таких как PHP PDO например. Если вы захотите откатывать изменения в случае ошибки прямо в MySQL, можно создать специальную процедуру и уже в ней выполнять ROLLBACK в обработчике:

    CREATE PROCEDURE prc_test () BEGIN DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK ; //Вот здесь откатываем транзакцию в случае ошибки END ; START TRANSACTION ; INSERT INTO tmp_table VALUES ("null" ) ; COMMIT ; END ; CALL prc_test () ;

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

    Рассмотрим практический пример: есть 2 таблицы, пользователи — users и информация о пользователях — user_info. Представим, что нам нужно либо выполнить 3 запроса к базе данных, либо не выполнять их вообще, так как иначе это приведет к сбоям в работе приложения.

    Start transaction; INSERT INTO user (id, nik) VALUES (1, "nikola"); INSERT INTO user_info (id, id_user, item_name, item_value) VALUES (1, 1, "Имя", "Николай"); INSERT INTO user_info (id, id_user, item_name, item_value) VALUES (2, 1, "Возраст", "24"); commit;

    В целом я думаю принцип работы транзакции понятен. Но все не так просто. Существуют проблемы параллельных транзакций. Рассмотрим пример. Представим, что во время выполнения этой транзакции, другой пользователь создал вторую параллельную транзакцию и сделал запрос SELECT * FROM user после того, как в нашей транзакции был выполнен первый запрос «INSERT INTO user (id, nik) VALUES (1, ‘nikola’)». Что увидит пользователь второй транзакции? Сможет ли он увидеть вставленную запись даже тогда, когда результаты первой транзакции еще не зафиксировались (не произошел COMMIT)? Или он сможет увидеть изменения только после того, как результаты первой транзакции будут зафиксированы? Оказывается имеют место быть оба варианта. Все зависит от уровня изоляции транзакции.

    У транзакций есть 4 уровня изоляции:

    • 0 - Чтение неподтверждённых данных (грязное чтение) (Read Uncommitted, Dirty Read) - самый низкий уровень изоляции. При этом уровне возможно чтение незафиксированных изменений параллельных транзакций. Как раз в этом случае второй пользователь увидит вставленную запись из первой незафиксированной транзакции. Нет гарантии, что незафиксированная транзакция будет в любой момент откачена, поэтому такое чтение является потенциальным источником ошибок.
    • 1 - Чтение подтверждённых данных (Read Committed) - здесь возможно чтение данных только зафиксированных транзакций. Но на этом уровне существуют две проблемы. В этом режиме строки, которые участвуют в выборке в рамках транзакции, для других параллельных транзакций не блокируются, из этого вытекает проблема № 1: «Неповторяемое чтение» (non-repeatable read) — это ситуация, когда в рамках транзакции происходит несколько выборок (SELECT) по одним и тем же критериям, и между этими выборками совершается параллельная транзакция, которая изменяет данные, участвующие в этих выборках. Так как параллельная транзакция изменила данные, результат при следующей выборке по тем же критериям в первой транзакции будет другой. Проблема № 2 — «Фантомное чтение» — этот случай рассмотрен ниже.
    • 2 - Повторяемое чтение (Repeatable Read, Snapshot) - на этом уровне изоляции так же возможно чтение данных только зафиксированных транзакций. Так же на этом уровне отсутствует проблема «Неповторяемого чтения», то есть строки, которые участвуют в выборке в рамках транзакции, блокируются и не могут быть изменены другими параллельными транзакциями. Но таблицы целиком не блокируются. Из-за этого остается проблема «фантомного чтения». «Фантомное чтение» — это когда за время выполнения одной транзакции результат одних и тех же выборок может меняться по причине того, что блокируется не вся таблица, а только те строки, которые участвуют в выборке. Это означает, что параллельные транзакции могут вставлять строки в таблицу, в которой совершается выборка, поэтому два запроса SELECT * FROM table могут дать разный результат в разное время при вставке данных параллельными транзакциями.
    • 3 - Сериализуемый (Serializable) - сериализуемые транзакции. Самый надежный уровень изоляции транзакций, но и при этом самый медленный. На этом уровне вообще отсутствуют какие либо проблемы параллельных транзакций, но за это придется платить быстродействием системы, а быстродействие в большинстве случаев крайне важно.

    По умолчанию в MySQL установлен уровень изоляции № 2 (Repeatable Read). И, как я считаю, разработчики MySQL не зря сделали по умолчанию именно этот уровень, так как он наиболее удачный для большинства случаев. С первого раза может показаться, что самый лучший вариант № 3 — он самый надежный, но на практике вы можете испытать большие неудобства из-за очень медленной работы вашего приложения. Помните, что многое зависит не от того, насколько хорош уровень изоляции транзакций в БД, а от того, как спроектировано ваше приложение. При грамотном программировании, можно даже использовать самый низкий уровень изоляции транзакций — все зависит от особенностей структуры и грамотности разработки вашего приложения. Но ненужно стремиться к самому низкому уровню изоляции — нет, просто если вы используйте не самый защищенный режим, следует помнить о проблемах параллельных транзакций, в этом случае вы не растеряетесь и все сделайте правильно.

    SET TRANSACTION — этот оператор устанавливает уровень изоляции следующей транзакции, глобально либо только для текущего сеанса.

    • SET TRANSACTION ISOLATION LEVEL
      { READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE }

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

    Вы можете также установить начальный глобальный уровень изоляции для сервера mysqld, запустив его с опцией -transaction-isolation