Всем привет!

Стартовые данные

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: уровни изоляции транзакций».