Django Models and Solving Competitive Data Access Issues

    Hello!

    There are already a lot of articles about the Django model on the hub, but I want to share with the public how to use them effectively and not step on the rake.

    Start data


    • 2 servers with Django running under uWSGI
    • 1-2k queries per second
    • Project with the movement of money inside


    What's next?


    Suppose we implement a balance update method for a user. And this method looks like this:

    class Profile(models.Model):
    ….
        def update_balance(self, balance):
            self.balance += balance
            self.save()
    


    In this case, if we receive two simultaneous requests to update the balance, the balance will update only the second request, because the last request crowded out the first and took the old data.

    At this stage, the F method comes to the rescue in conjunction with .update ()
    F () returns us the value from the database in the current state. and the previous section can be written like this

    class Profile(models.Model):
    ….
        def update_balance(self, balance):
            Profile.objects.\
                filter(pk=self.pk)\
               .update(balance=F('balance') + balance)
    

    In this case, we always get the actual value of the field and some will say that this method solves the problem for us, but this is not so. In this case, although everything is implemented correctly, as we believe, this does not solve the problem.

    In this case, a transaction at the database level comes to our aid.

    What is a transaction and how to use it?


    To begin with, you can enable Transaction Middleware in Django 1.4.x and 1.5.x. In Django 1.6+, it was replaced with the ATOMIC_REQUESTS constant, which can be included with each database used in the project.

    They work as follows. When a request came to us and before sending this processing request to view, Django opens a transaction. If the request was processed without exceptions, then a commit is made in the database or rollback if an exception pops up.

    The difference between ATOMIC_REQUESTS and Middleware is that Middleware is enabled for the entire project, and ATOMIC_REQUESTS can be used for one or more databases.

    The downside of using this approach is that an overhead to the database is created.
    In this case, manual transaction management comes to our aid.

    Manual Transaction Management


    Django provides many work options using the django.db.transaction module.

    Consider one of the possible methods of manual control - transaction.atomic

    transaction.atomic is both a method and a decorator and is used only for view methods.

    You can secure the purchase of goods by wrapping view in the decorator. for instance

    ...
    from django.db import transaction
    ...
    @transaction.atomic
    def buy_something(request):
        ....
        request.user.update_balance(money)
        return render(request, template, data)
    


    In this case, we included the atomicity of the transaction for the purchase of goods. All responsibility for data integrity was transferred to the database and atomicity solves our problem.

    Even in conjunction with atomic transactions, you can use the select_for_update method.
    In this case, the line being modified will be locked for change until update is called.
    Our balance update method can now be written like this:
    class Profile(models.Model):
    ….
        def update_balance(self, balance):
            Profile.objects.select_for_update().\
                filter(pk=self.pk)\
               .update(balance=F('balance') + balance)
    


    Conclusions:


    • Atomicity comes to the rescue
    • Make atomic only critical pieces of code
    • Use select for update to lock data during change
    • If possible, try to make transactions as short as possible so as not to block the work with data in the database.


    Additionally: about the transaction levels in MySQL told "MySQL: transaction isolation levels . "

    Also popular now: