21
2012
Django и особенности использования транзакций в MySQL
Наверное всем известно, что Django является одним из самых популярных фреймворков для web-разработки на python-е. И даже если в основе web-проекта лежит сторонний код, то зачастую при разработке используют отдельные части этого фреймворка — например ORM. В данной статье я хотел бы рассказать об особенностях использования Django ORM при работе с базой данных MySQL, а именно про транзакции и подводные камни, связанные с ними. Так, например, если в какой-то момент вы осознаёте, что вместо ожидаемых данных, возвращается совершенно другой результат, то возможно, данная статья поможет разобраться что к чему.
Далее речь пойдет про InnoDB, поскольку это единственный движок, идущий в составе MySQL и полноценно поддерживающий транзакции (BDB не в счёт, так как давно уже не поддерживается).
Стоит отметить ряд особенностей:
1. В Django в качестве интерфейса к MySQL используется расширение MySQLdb, а оно в свою очередь при каждом подключении к базе устанавливает:
AUTOCOMMIT=0
То есть каждая операция изменения данных должна завершаться COMMIT/ROLLBACK для фиксации или отката изменений. Если вы раньше использовали расширения PHP (PDO, Mysqli) или Ruby для доступа к MySQL, то наверное будете немного удивлены, поскольку практически во всех драйверах доступа к БД при подключении значение AUTOCOMMIT не меняется (а по умолчанию в MySQL оно задано как AUTOCOMMIT=1).
2. MySQL использует уровень изоляции транзакций REPEATABLE-READ, в отличии, например, от PosgreSQL или Oracle, в которых уровень изоляции транзакий по умолчанию READ-COMMITTED.
Что это значит? Рассмотрим на конкретном примере:
CREATE TABLE IF NOT EXISTS `test` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `value` VARCHAR(255) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO `test` VALUES (NULL, 'a');
REPEATABLE-READ
1ая транзакция: | 2ая транзакция: |
---|---|
SET AUTOCOMMIT=0; | SET AUTOCOMMIT=0; |
SELECT * FROM `test`;
+----+-----------+ | id | value | +----+-----------+ | 1 | a | +----+-----------+ |
|
INSERT INTO `test` VALUES (NULL, ‘b’); SELECT * FROM `test`; +----+-----------+ | id | value | +----+-----------+ | 1 | a | | 2 | b | +----+-----------+ COMMIT; |
|
INSERT INTO `test` VALUES (NULL, ‘c’); SELECT * FROM `test`; +----+-----------+ | id | value | +----+-----------+ | 1 | a | | 3 | c | +----+-----------+ COMMIT; |
Как видно из примера, в первой транзакции после первого чтения данных все последующие чтения будут возвращать точно такой же результат до того момента, пока не произойдет COMMIT, независимо от того что происходит в других транзакциях.
READ-COMMITTED
1ая транзакция: | 2ая транзакция: |
---|---|
SET SESSION tx_isolation=’READ-COMMITTED’; SET AUTOCOMMIT=0; |
SET SESSION tx_isolation=’READ-COMMITTED’; SET AUTOCOMMIT=0; |
SELECT * FROM `test`;
+----+-----------+ | id | value | +----+-----------+ | 1 | a | +----+-----------+ |
|
INSERT INTO `test` VALUES (NULL, ‘b’); | |
SELECT * FROM `test`;
+----+-----------+ | id | value | +----+-----------+ | 1 | a | +----+-----------+ |
|
SELECT * FROM `test`;
+----+-------+ | id | value | +----+-------+ | 1 | a | | 2 | b | +----+-------+ COMMIT; |
|
INSERT INTO `test` VALUES (NULL, ‘c’); SELECT * FROM `test`; +----+-------+ | id | value | +----+-------+ | 1 | a | | 2 | b | | 3 | c | +----+-------+ COMMIT; |
В случае READ-COMMITTED выборка SELECT всегда возвращает последнюю закоммиченную версию данных.
Возвращаясь к теме Django — подвох в использовании Django ORM состоит в том, что судя по всему READ-COMMITTED единственный уровень изоляции транзакций, на который ориентировались разработчики. Так, например, если мы обратимся к коду Django, а именно к реализации метода get_or_create() в классе QuerySet:
def get_or_create(self, **kwargs): """ Looks up an object with the given kwargs, creating one if necessary. Returns a tuple of (object, created), where created is a boolean specifying whether an object was created. """ assert kwargs, \ 'get_or_create() must be passed at least one keyword argument' defaults = kwargs.pop('defaults', {}) lookup = kwargs.copy() for f in self.model._meta.fields: if f.attname in lookup: lookup[f.name] = lookup.pop(f.attname) try: self._for_write = True return self.get(**lookup), False except self.model.DoesNotExist: try: params = dict([(k, v) for k, v in kwargs.items() if '__' not in k]) params.update(defaults) obj = self.model(**params) sid = transaction.savepoint(using=self.db) obj.save(force_insert=True, using=self.db) transaction.savepoint_commit(sid, using=self.db) return obj, True except IntegrityError, e: transaction.savepoint_rollback(sid, using=self.db) exc_info = sys.exc_info() try: return self.get(**lookup), False except self.model.DoesNotExist: # Re-raise the IntegrityError with its original traceback. raise exc_info[1], None, exc_info[2]
то вторая попытка извлечения объекта:
return self.get(**lookup), False
всегда будет завершаться с ошибкой.
Попытаюсь пояснить — вот, например, два процесса одновременно вызывают метод get_or_create() некой модели. 1ый процесс пытается прочитать данные — данных нет, генерируется исключение DoesNotExist. 2ой процесс аналогично пытается прочитать данные и аналогично генерирует исключение DoesNotExist. Далее, поскольку в рамках соединения используется AUTOCOMMIT=0 и уровень изоляции транзакций REPEATABLE-READ, оба процесса «замораживают» прочитанные данные. Допустим, пусть первый процесс успешно создает запись и возвращает объект созданной записи. Но при этом второй процесс не может ничего создать, так как это будет нарушать ограничение уникальности. Забавно то, что он не видит объект, созданный в первом процессе, в связи с тем, что при повторном чтении данных возвращается «замороженный» результат.
Конечно, в экспериментальных условиях данную ошибку воспроизвести довольно проблематично, но при многочисленных конкурентных запросах, данный код будет работать нестабильно, периодически генерируя исключение DoesNotExist.
Как с этим бороться?
1. В случае использования метода get_or_create() — написать свой метод, выполняющий принудительный COMMIT перед повторным чтением данных:
@transaction.commit_manually() def custom_get_or_create(...): try: obj = SomeModel.objects.create(...) except IntegrityError: transaction.commit() obj = SomeModel.objects.get(...) return obj
2. В настройках MySQL ( /etc/mysql/my.cnf ) использовать принудительно уровень изоляции транзакций READ-COMMITTED:
transaction-isolation = READ-COMMITTED
3. При использовании Django >= 1.2 версии использовать в settings.py в опциях подключения к базе следующий код:
DATABASE_OPTIONS = { "init_command": "SET storage_engine=INNODB, SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED", }
Данный баг опубликован на багтрекере Django достаточно давно, но до сих пор тикет не закрыт и проблема по-прежнему актуальна.
Или вот ещё пример — допустим Django ORM используется отдельно от веб-сервера, в рамках некоего демона, который постоянно висит в памяти, и периодически считывает новые данные из таблицы MySQL. Данный эксперимент можно провести используя встроенный shell в Django:
python manage.py shell >>> from test_module.models import * >>> len(SomeModel.objects.all()) 10
Далее используя второй терминал добавим несколько записей:
>>> SomeModel(name='test1').save() >>> SomeModel(name='test2').save() >>> len(SomeModel.objects.all()) 12
И хотя во втором терминале изменения налицо, но эти новодобавленные записи в первом терминале по прежнему будут недоступны, т.к. начатая транзакция не завершена, и после первого чтения данных все последующие чтения будут возвращать один и и тот же результат до тех пор, пока принудительно не будет вызван COMMIT.
Что с этим делать? Менять уровень изоляции транзакций в настройках mysql (my.cnf) или же в параметрах подключения к базе данных в settings.py в Django. Ну или же принудительно коммитить данные после каждого чтения:
>>> from django.db import connection, transaction >>> len(Param_Type.objects.all()) 10 >>> transaction.commit_unless_managed() >>> len(Param_Type.objects.all()) 12
Почему все так происходит? Возможно это связано с тем, что Django изначально проектировался для работы с использованием PostgreSQL в качестве базы данных, в котором, как писалось выше, «из коробки» используется READ-COMMITTED. В общем, данное не совсем стандартное поведение Django ORM применительно к MySQL InnoDB может привести к довольно-таки трудноотлавливаемым багам. Поэтому в большинстве мест, где обсуждается описанная проблема (в различных блогах и на stackoverflow), настойчиво рекомендуют использовать READ-COMMITTED как уровень изоляции транзакций по умолчанию, аргументируя этот выбор ещё и тем, что READ-COMMITTED «производительнее» чем REPEATABLE-READ.
P.S Тот же самый написанный мной пост на хабре.
Оставить комментарий
Пн | Вт | Ср | Чт | Пт | Сб | Вс |
---|---|---|---|---|---|---|
« Сен | ||||||
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 |
Метки
Рубрики
- Apache (1)
- Highload (4)
- JavaScript (1)
- Linux (3)
- MongoDB (1)
- MySQL (1)
- Perl (1)
- PHP (5)
- Python (5)
- Web-разработка (5)
- Алгоритмы (1)
- За жизнь (2)
- Конференции (6)