Модели 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: уровни изоляции транзакций».
    Метки:
    Поделиться публикацией
    Похожие публикации
    Комментарии 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 можно использовать, где логгируется все перемещение денег

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