Pull to refresh

Модели Django и решение проблем с конкурентным доступом к данным

Reading time 3 min
Views 27K
Всем привет!

Про 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: уровни изоляции транзакций».
Tags:
Hubs:
+13
Comments 33
Comments Comments 33

Articles