Разработчик
0,0
рейтинг
10 марта 2015 в 18:10

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

Всем привет!

Про 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: уровни изоляции транзакций».
Минкин Андрей @Gen1us2k
карма
21,0
рейтинг 0,0
Разработчик
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (33)

  • +2
    Есть ли возможность обернуть в транзакцию набор последовательных операций на уровне ORM?
  • –8
    Не называйте словом ORM модели в Django. ORM – это паттерн програмирования, который _не_ реализован в Django models. Если вы посмотрите официальную документацию, вы нигде не найдете там этого слова. Пожалуйста, не вводите людей в заблуждение.
    • 0
      Действительно. Спасибо за замечание :)
    • +9
      Да есть оно там. Вот пример.

      Я не спора для, проясните вопрос: почему это не ORM? Дайте, пожалуйста, определение ORM, такое, чтобы Django под определение не попало. Если использовать наивное понимание Object-Relational Mapping, то код в Django именно что выполняет отображение объектов в реляционную БД и обратно. Сами они утверждают, что код в Django — ActiveRecord. Если ActiveRecord не является разновидностью ORM, объясните разницу, пожалуйста.
      • 0
        Честно говоря, я не понял что должна была показать мне ваша ссылка.

        Поясняю.

        ORM призванный решить проблемы «persistence» вашего приложения. Например, выступая прослойкой между БД и приложением, ORM гарантирует, что если приложение в рамках одной сессии несколько раз выбирает один и тот же «объект» из БД, то физически это будет один и тот же объект.

        В Django это выглядело бы так:

        >>> entry1 = Entry.objects.get(pk=1)
        >>> entry2 = Entry.objects.get(pk=1)
        >>> entry1 is entry2
        True
        


        Заметьте, я ипользовал оператор `is`, а не `==`. Этот принцип выполняется для разных запросов любой сложности, это не просто результат кеширования. Django ничего подобного вам не гарантирует, так что никакой это не object-relational mapping.

        То что реализовано в Django – именно что реализация паттерна Active Record, который хоть и можеть быть частью реализацию ORM, самом по себе ORM совершенно не является. Но все привыкли называть это ORM в основном с подачи авторов документаций по PHP фреймворкам, у которых к сожалению не редко возникают проблемы с терминологией.

        Из того что мне известно, ORM реализуют:
        — SqlAlchemy (а точнее та ее часть, которая называется SqlAlchemy ORM)
        — .NET Entity Framework
        — Hibernate

        Не имеют никакого отношения к ORM и реализуют паттерн Active Record:
        — Django Models
        — Yii и большинство других PHP фрейморков
        • 0
          Пока что вы меня только убедили в обратном: код в Django — ORM, т.к. сопоставляет объекты и записи в реляционной БД. Другими словами, сопоставление объектов и записей есть? Значит, ORM.

          Дайте всё-таки определение ORM, пожалуйста, так, чтобы было понятно, что сравнение объектов должно выполняться не при помощи операторов типа ==, а исключительно с помощью операторов типа is. (И не относящееся к конкретному языку.)
          • 0
            А Django models не маппит записи бд в объекты. Если хотите, django models маппит записи в бд в состояние объектов на момент их выборки из бд. Состояния одного и того же объекта могут быть несколькими объектами, в то время как сам объект только один. В этом одно из принципиальных различий, и на самом деле оно вытекает даже из самого названия (и да, я согласен что это совершенно не очевидно на первый взгляд). ORM – это не просто штука, позволяющая работать с данными в таблицах «как с объектами», это именно что биекция данных бд в объектную модель, где каждой записи сопоставлен один и только один объект. В этом смылсе ORM, так же как и БД занимается вопросами консистентности данных и борется с коллизиями на свое уровне абстракции, и она обязана этим заниматся, будучи отображением базы данных в приложеии, в то время как Active Record не занимается этими вопросами вообще, создавая тем самым abstraction leak.

            В любом случае, это вопрос определений, и честно говоря, я не хочу продолжать этот спор. В Computer Science eсть много понятий, которые за последнее время сильно исказились. Наверное для вас будет открытием если я скажу, что большинство веб-фреймворков не являются MVC, даже не смотря на то, что их разработчики бьют себя в грудь и утверждают об обратном. Дело в том, что MVC предполагает прямое взаимодействие между моделью и отображением посредством событий, а в большинстве веб-фрейворков ничего подобного нет.
            • 0
              Кстати, это именно та причина, почему Django разработчики честно называют свой фреймворк MTV, хотя никаких принципиальных различий в архитектуре между ним и большинством «MVC» фрейворков нет.
    • +3
      Если что-то не названо, то это не значит, что этого нет.
      ORM в Django есть, реализована через паттерн проектирования ActiveRecord (разновидность ORM).
      • 0
        С чего вы взяли, что Active Record – разновидность ORM? Это не правда. Active Record может быть реализован как часть ORM, точно так же, как и существовать по отдельности от ORM. Выше я ответил вашему товарищу более подробно.
        • 0
          > С чего вы взяли, что Active Record – разновидность ORM
          По определению Active Record и определению ORM.

          ActiveRecord — всего лишь частный случай ORM. Есть и другие варианты: Table Data Gateway, Row Data Gateway, Data Mapper.
          Более того, каждый Active Record — это не только частный случай ORM, но и частный случай Row Data Gateway.
          • +1
            Вы перечислили data-access патерны, а ORM на уровень выше, там где domain model.
  • +1
    Еще кое что по транзазакциям и MySQL:
    docs.djangoproject.com/en/dev/topics/db/transactions/#using-a-high-isolation-level
    Переведите MySQL в read committed
  • 0
    а почему
            Profile.objects.\
                filter(pk=self.pk)\
               .update(balance=F('balance') + balance)
    

    а не
    self.balance = F('balance') + balance
    
    • 0
      Потому что потом вы скорее всего напишите
      self.save()
      а save перезаписывает все данные в базе на текущие значения в модели
      • +1
        нет, я напишу self.save(update_fields=['balance'])
        • 0
          self.balance = F('balance') + balance
          #… тут кто-то в другом процессе тоже обновил этот же баланс
          self.save(update_fields=['balance']) тут данные этого кого-то перезапишутся вашими
          • +1
            это не отличается от:
                    #… тут кто-то в другом процессе тоже обновил этот же баланс
                    Profile.objects.\
                        filter(pk=self.pk)\
                       .update(balance=F('balance') + balance)
            

            в обоих случаях на уровне SQL будет update set balance = balance + «balance_из_параметра»
            • 0
              Да, я просмотрел, что у Вас тоже F используется. В этом случае действительно гонок не будет
      • +1
        если что, я не докапываюсь, я просто пытаюсь выяснить, может я что-то упустил.
  • +1
    А где сказано, что @transaction.atomic используется только для view методов? По идее (и по исходным кодам), любой блок кода можно завернуть в with transaction.atomic() и любую функцию/метод в него же, используя синтаксис декоратора. И разработчики django явно нигде не препятствуют этому.
  • +1
    И еще:

    В некоторых участках когда, возможно, стоит писать так:
    def update_balance(self, balance):
            Profile.objects.select_for_update().\
                filter(pk=self.pk, balance=self.balance)\
               .update(balance=F('balance') + balance)
    


    Чтобы не допустить, что balance может стать отрицательным
    • +1
      не понятна мысль, выборка идет по primary key, это однозначное определение одной записи, и как ваше дополнительное условие связно с отрицательным балансом?
      • +2
        Фильтр не найдет запись с таким балансом — т.е. в паралелльном запросе у пользователя он изменился по каким-либо причинам, и, возможно, требуется отдельная обработка такой ситуации
        • +1
          очень спорно, он может изменится по вполне легальной причине
          • 0
            На самом деле это очень правильно решение. Не меняйте данные не глядя. Перед тем, как прибавить или вычесть баланс вы же должны были сделать какие-то проверки. Как минимум, что баланс будет не меньше нуля (как было сказано выше). Но могут быть и другие, что пользователь потратил не больше, чем установил лимит, что положил не больше, чем возможно по законодательству. Мало ли. И поэтому, важно не только проверить по текущему значению баланса, что все ок, но и то, что этот баланс не поменялся за время проверок.

            Но надо подумать, как это будет сочетаться с транзакциями, возможно они будут тут только мешать.
  • 0
    Поясните, почему в вашем примере связка F + update не решает проблему?
    def update_balance(self, balance):
        Profile.objects.filter(pk=self.pk).update(balance=F('balance') + balance)
    

    • 0
      Решает. Но как правило вместе с обновлением баланса требуется выполнить еще какое-либо действие (сменить статус у заказа, например). Здесь и понадобятся транзакции
  • +4
    Ожидал большего от такого заголовка и вступления, чем пара пунктов из документации.
    Зачем вообще было писать про самый обычный и всеми применимый способ?
  • +1
    Тема дедлоков не раскрыта. Хорошо конечно, когда select… for update на request только один.

    Вообще, можно не бороться с конкурентростью, а делегировать все обновления баланса единственнуму инстансу celery worker'a.

    Если вы не платежная система с сотней тысяч пользователей, производительности одного worker'a хватит за глаза.
  • +4
    Ну кто ж так делает-то?!: balance = balance + x
    За свой опыт работы с системами, где нужно отслеживать движение финансов я уяснил для себя такую вещь — везде, где финансы есть, нужно использовать отдельную таблицу, где будет отражено любое движение средств, и баланс — это должна быть не циферка в базе, а сумма всех транзакций, которую можно поместить в отдельную view с кешированием, если нужно быстро. Иначе куча проблем потом может быть.
    • 0
      Moneylog можно использовать, где логгируется все перемещение денег

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.