Всем привет!
Про Django модели уже много статей на хабре, но хочется поделится с общественностью, как эффективно использовать их и не наступать на грабли.
Допустим мы реализуем метод обновления баланса для пользователя. И этот метод выглядит так:
В этом случае, если нам придут два одновременных запроса на обновление баланса, то баланс обновит только второй запрос, потому что последний запрос вытеснил первый и взял старые данные.
На этом этапе на помощь нам приходит метод F в связке с .update()
F() возвращает нам значение из базы в актуальном состоянии. и предыдущий участок можно записать так
В этом случае мы всегда получаем актуальное значение поля и некоторые скажут, что этот способ решает нам проблему, но это не так. В этом случае, хоть и реализовано все правильно, как мы считаем, но проблему это не решает.
В этом случае приходит к нам на помощь транзакции на уровне БД.
Начнем с того, что в Django 1.4.x и 1.5.x можно включить Transaction Middleware. В Django 1.6+ ее заменили на константу ATOMIC_REQUESTS, которую можно включить к каждой БД использующейся в проекте.
Работают они следующим образом. Когда к нам пришел запрос и перед тем как передать этот запрос на обработку во view Django открывает транзакцию. Если запрос был отработан без исключений, то делается commit в БД или rollback, если выскочило исключение.
Разница между ATOMIC_REQUESTS и Middleware в том, что Middleware включается для всего проекта, а ATOMIC_REQUESTS можно использовать для одной или нескольких БД.
Минус использования этого подхода в том, что создается оверхед на базу данных.
В этом случае нам на помощь приходит ручное управление транзакциями.
Django предоставляет множество вариантов работы с помощью модуля django.db.transaction
Рассмотрим один из возможных способов ручного управления — это transaction.atomic
transaction.atomic является и методом и декоратором и используется только для view методов.
Обезопасить покупку товара можно, обернув view в декоратор. Например
В этом случае мы включили атомарность транзакции для покупки товара. Всю ответственность за целостность данных переложили на БД и атомарность решает нашу проблему.
Еще в связке с атомарными транзакциями можно использовать select_for_update метод.
В этом случае изменяемая строка будет блокироваться на изменение до тех пор, пока не вызовется update.
Наш метод обновления баланса можно записать теперь так:
Выводы:
Дополнительно: про уровни транзакций в MySQL рассказали «MySQL: уровни изоляции транзакций».
Про Django модели уже много статей на хабре, но хочется поделится с общественностью, как эффективно использовать их и не наступать на грабли.
Стартовые данные
- 2 сервера с Django, запущенные под uWSGI
- 1-2k запросов в секунду
- Проект с движением денег внутри
Что дальше?
Допустим мы реализуем метод обновления баланса для пользователя. И этот метод выглядит так:
class Profile(models.Model):
….
def update_balance(self, balance):
self.balance += balance
self.save()
В этом случае, если нам придут два одновременных запроса на обновление баланса, то баланс обновит только второй запрос, потому что последний запрос вытеснил первый и взял старые данные.
На этом этапе на помощь нам приходит метод F в связке с .update()
F() возвращает нам значение из базы в актуальном состоянии. и предыдущий участок можно записать так
class Profile(models.Model):
….
def update_balance(self, balance):
Profile.objects.\
filter(pk=self.pk)\
.update(balance=F('balance') + balance)
В этом случае мы всегда получаем актуальное значение поля и некоторые скажут, что этот способ решает нам проблему, но это не так. В этом случае, хоть и реализовано все правильно, как мы считаем, но проблему это не решает.
В этом случае приходит к нам на помощь транзакции на уровне БД.
Что это такое транзакции и как это использовать?
Начнем с того, что в Django 1.4.x и 1.5.x можно включить Transaction Middleware. В Django 1.6+ ее заменили на константу ATOMIC_REQUESTS, которую можно включить к каждой БД использующейся в проекте.
Работают они следующим образом. Когда к нам пришел запрос и перед тем как передать этот запрос на обработку во view Django открывает транзакцию. Если запрос был отработан без исключений, то делается commit в БД или rollback, если выскочило исключение.
Разница между ATOMIC_REQUESTS и Middleware в том, что Middleware включается для всего проекта, а ATOMIC_REQUESTS можно использовать для одной или нескольких БД.
Минус использования этого подхода в том, что создается оверхед на базу данных.
В этом случае нам на помощь приходит ручное управление транзакциями.
Ручное управление транзакциями
Django предоставляет множество вариантов работы с помощью модуля django.db.transaction
Рассмотрим один из возможных способов ручного управления — это transaction.atomic
transaction.atomic является и методом и декоратором и используется только для view методов.
Обезопасить покупку товара можно, обернув view в декоратор. Например
...
from django.db import transaction
...
@transaction.atomic
def buy_something(request):
....
request.user.update_balance(money)
return render(request, template, data)
В этом случае мы включили атомарность транзакции для покупки товара. Всю ответственность за целостность данных переложили на БД и атомарность решает нашу проблему.
Еще в связке с атомарными транзакциями можно использовать select_for_update метод.
В этом случае изменяемая строка будет блокироваться на изменение до тех пор, пока не вызовется update.
Наш метод обновления баланса можно записать теперь так:
class Profile(models.Model):
….
def update_balance(self, balance):
Profile.objects.select_for_update().\
filter(pk=self.pk)\
.update(balance=F('balance') + balance)
Выводы:
- Атомарность приходит на помощь
- Делайте атомарными только критически важные участки кода
- Используйте select for update для блокировки данных во время изменения
- По возможности старайтесь делать транзакции как можно короче, чтобы не блокировать работу с данными в БД.
Дополнительно: про уровни транзакций в MySQL рассказали «MySQL: уровни изоляции транзакций».