How to manage concurrency in Django models

The days of desktop systems serving single users are long gone — web applications nowadays are serving millions of users at the same time. With many users comes a wide range of new problems — concurrency problems.

In this article I’m going to present two approaches for managing concurrency in Django models.

Photo byDenys Nevozhai


The Problem

To demonstrate common concurrency issues we are going to work on a bank account model:

class Account(models.Model):
    id = models.AutoField(        primary_key=True,    )    user = models.ForeignKey(        User,    )    balance = models.IntegerField(        default=0,    )

To get started we are going to implement a naive deposit and withdrawmethods for an account instance:

def deposit(self, amount):    self.balance += amount    self.save()
def withdraw(self, amount):    if amount > self.balance:        raise errors.InsufficientFunds()
    self.balance -= amount    self.save()

This seems innocent enough and it might even pass unit tests and integration tests on localhost. But, what happens when two users perform actions on the same account at the same time?

  1. User A fetches the account — balance is 100$.
  2. User B fetches the account — balance is 100$.
  3. User B withdraws 30$ — balance is updated to 100$ — 30$ = 70$.
  4. User A deposits 50$ — balance is updated to 100$ + 50$ = 150$.

What happened here?

User B asked to withdraw 30$ and user A deposited 50$ — we expect the balance to be 120$, but we ended up with 150$.

Why did it happen?

At step 4, when user A updated the balance, the amount he had stored in memory was stale (user B had already withdrawn 30$).

To prevent this situation from happening we need to make sure the resource we are working on is not altered while we are working on it.


Pessimistic approach

The pessimistic approach dictates that you should lock the resource exclusively until you are finished with it. If nobody else can acquire a lock on the object while you are working on it, you can be sure the object was not changed.

To acquire a lock on a resource we use a database lock for several reasons:

  1. (relational) databases are very good at managing locks and maintaining consistency.
  2. The database is the lowest level in which data is accessed — acquiring the lock at the lowest level will protect the data from other processesmodifying the data as well. For example, direct updates in the DB, cron jobs, cleanup tasks, etc.
  3. A Django appcan run on multiple processes (e.g workers). Maintaining locks at the app level will require a lot of (unnecessary) work.

To lock an object in Django we use select_for_update.

Let’s use the pessimistic approach to implement a safe deposit and withdraw:

@classmethoddef deposit(cls, id, amount):with transaction.atomic():       account = (           cls.objects.select_for_update()           .get(id=id)       )

       account.balance += amount       account.save()
    return account
@classmethoddef withdraw(cls, id, amount):with transaction.atomic():       account = (           cls.objects           .select_for_update()           .get(id=id)       )

       if account.balance < amount:           raise errors.InsufficentFunds()
       account.balance -= amount       account.save()

   return account

What do we have here:

  1. We use select_for_update on our queryset to tell the database to lock the object until the transaction is done.
  2. Locking a row in the database requires a database transaction — we use Django’s decorator transaction.atomic() to scope the transaction.
  3. We use a classmethod instead of an instance method — to acquire the lock we need to tell the database to lock it. To achieve that we need to be the ones fetching the object from the database. When operating on self the object is already fetched and we don’t have any guaranty that it was locked.
  4. All the operations on the account are executed within the database transaction.

Let’s see how the scenario from earlier is prevented with our new implementation:

  1. User A asks to withdraw 30$:
    User A acquires a lock on the account.
    - Balance is 100$.
  2. User B asks to deposit 50$:
    - Attempt to acquire lock on account fails (locked by user A).
    User B waits for the lock to release.
  3. User A withdraw 30$ :
    - Balance is 70$.
    Lock of user A on account is released.
  4. User B acquires a lock on the account.
    - Balance is 70$.
    - New balance is 70$ + 50$ = 120$.
  5. Lock of user B on account is released, balance is 120$.

Bug prevented!

What you need to know about select_for_update:

  • In our scenario user B waited for user A to release the lock. Instead of waiting we can tell Django not to wait for the lock to release and raise a DatabaseError instead. To do that we can set the nowait argument of select_for_update to True, …select_for_update(nowait=True).
  • Select related objects are also locked — When using select_for_update with select_related, the related objects are also locked.
    For example, If we were to select_related the user along with the account, both the user and the account will be locked. If during deposit, for example, someone is trying to update the first name, that update will fail because the user object is locked.
    If you are using PostgreSQL or Oracle this might not be a problem soon thanks to a new feature in the upcoming Django 2.0. In this version, select_for_update has an “of” option to explicitly state which of the tables in the query to lock.

I used the bank account example in the past to demonstrate common patterns we use in Django models. You are welcome to follow up in this article:

Bullet Proofing Django Models
We recently added a bank account like functionality into one of our products. During the development we encountered…medium.com


Optimistic Approach

Unlike the pessimistic approach, the optimistic approach does not require a lock on the object. The optimistic approach assumes collisions are not very common, and dictates that one should only make sure there were no changes made to the object at the time it is updated.

How can we implement such a thing with Django?

First, we add a column to keep track of changes made to the object:

version = models.IntegerField(    default=0,)

Then, when we update an object we make sure the version did not change:

def deposit(self, id, amount):updated = Account.objects.filter(       id=self.id,       version=self.version,   ).update(       balance=balance + amount,       version=self.version + 1,   )
   return updated > 0
def withdraw(self, id, amount):          if self.balance < amount:       raise errors.InsufficentFunds()

updated = Account.objects.filter(       id=self.id,       version=self.version,   ).update(       balance=balance - amount,       version=self.version + 1,   )

   return updated > 0

Let’s break it down:

  1. We operate directly on the instance (no classmethod).
  2. We rely on the fact that the version is incremented every time the object is updated.
  3. We update only if the version did not change:
    - If the object was not modified since we fetched it than the object is updated.
    - If it was modified than the query will return zero records and the object will not be updated.
  4. Django returns the number of updated rows. If `updated` is zero it means someone else changed the object from the time we fetched it.

How is optimistic locking work in our scenario:

  1. User A fetch the account — balance is 100$, version is 0.
  2. User B fetch the account — balance is 100$, version is 0.
  3. User B asks to withdraw 30$:
    - Balance is updated to 100$ — 30$ = 70$.
    Version is incremented to 1.
  4. User A asks to deposit 50$:
    - The calculated balance is 100$ + 50$ = 150$.
    - The account does not exist with version 0 -> nothing is updated.

What you need to know about the optimistic approach:

  • Unlike the pessimistic approach, this approach requires an additional field and a lot of discipline.
    One way to overcome the discipline issue is to abstract this behavior. django-fsm implements optimistic locking using a version field as described above. django-optimistic-lock seem to do the same. We haven’t used any of these packages but we’ve taken some inspiration from them.
  • In an environment with a lot of concurrent updates this approach might be wasteful.
  • This approach does not protect from modifications made to the object outside the app. If you have other tasks that modify the data directly (e.g no through the model) you need to make sure they use the version as well.
  • Using the optimistic approach the function can fail and return false. In this case we will most likely want to retry the operation. Using the pessimistic approach with nowait=False the operation cannot fail — it will wait for the lock to release.

Which one should I use?

Like any great question, the answer is “it depends”:

  • If your object has a lot of concurrent updates you are probably better off with the pessimistic approach.
  • If you have updates happening outside the ORM (for example, directly in the database) the pessimistic approach is safer.
  • If your method has side effects such as remote API calls or OS calls make sure they are safe. Some things to consider — can the remote call take a long time? Is the remote call idempotent (safe to retry)?

Like what you read? Give Haki Benita a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.

转载于:https://www.cnblogs.com/ExMan/p/9340440.html

How to manage concurrency in Django models相关推荐

  1. django models 之字段详解

         数据库模型是django操作数据库的主要途径,也是django和数据库连接的主要方法,几乎所有对数据库的操作都是通过models来完成的,下面我们就简单介绍一下创建模型时会涉及到的字段类型, ...

  2. Django models Form model_form 关系及区别

    Django models Form model_form 转载于:https://www.cnblogs.com/hellojesson/p/6234636.html

  3. Django models的诡异异常RelatedObjectDoesNotExist

    Django models的诡异异常RelatedObjectDoesNotExist 参考文章: (1)Django models的诡异异常RelatedObjectDoesNotExist (2) ...

  4. Django models模型

    Django models模型 一. 所谓Django models模型,是指的对数据库的抽象模型,models在英文中的意思是模型,模板的意思,在这里的意思是通过models,将数据库的借口抽象成p ...

  5. django models 配置

    一, 使用已有视图 场景: 项目中用到了一个视图:DT_users 对应的django models.py配置为: class DT_users(models.Model):...class Meta ...

  6. Django models 筛选不等于

    Django models 筛选不等于 目前的查询 j = Job.objects.filter(status="0").all() 筛选不等于 0 并不能用如下写法 j = Jo ...

  7. Django models数据库配置以及多数据库调用设置

    今天来说说web框架Django怎么配置使用数据库,也就是传说中MVC(Model View Controller)中的M,Model(模型). 简单介绍一下Django中的MVC: 模型(model ...

  8. django models索引_django-models – Django模型“IndexError:列表索引超出范围”Pydev

    我在 Eclipse PyDev中有一个Django项目. 我有一个文件views.py,其中包含以下行: from models import ingredient2 在models.py我有: f ...

  9. django models索引_Django(生命周期、每部分详解、路由层)

    https://www.zhihu.com/video/1248736141978927104 每日测验 """ 今日考题 1.什么是静态文件,django静态文件配置如 ...

最新文章

  1. C语言模拟实现库函数 atoi
  2. 版本号比较函数-js
  3. php软件升级管理系统,POSCMS开源内容管理系统 v3.6.1 升级说明
  4. .NET遇上Docker - 使用Docker Compose组织Ngnix和.NETCore运行
  5. Python基础:元类
  6. 小微企业名录查询系统_欢迎访问辽宁小微企业名录系统
  7. JavaScript中Object.keys、Object.getOwnPropertyNames区别
  8. Linux虚拟机下使用USB转串口线——配置minicom、以及screen的使用
  9. 【Python3网络爬虫开发实战】1.5.1-PyMySQL的安装
  10. 美国西北大学 计算机工程专业排名,权威首发!2018年USNews美国大学研究生计算机工程专业排名榜单...
  11. Mac新手使用技巧——AirDrop
  12. IO-01. 表格输出(5)
  13. C#3.0亮点 —— lambda表达式
  14. 分布式事务解决方案 Seata 的原理个人理解以及 demo 配置
  15. 测试环境由谁搭建?第三方软件测试环境搭建步骤流程
  16. WebStorm上vue模板设置
  17. Python的m3u8下载器源码
  18. In-Place Scene Labelling and Understanding with Implicit Scene Representation
  19. IntelliJ IDEA 社区版使用指南
  20. ACL国际计算机语言协会2019,干货 | 2019 AI 国际顶级学术会议一览表

热门文章

  1. aes解密设置utf8 php,PHP aes (ecb)解密后乱码问题
  2. ffmpeg转码器移植VC的工程:ffmpeg for MFC
  3. 一些VC的快捷键以及调试技巧
  4. 电池供电的电容麦_板儿砖变电池?!是的,科学家已成功实现这一功能
  5. 从客户端中(...)检测到有潜在危险的 Request.Form值
  6. Springboot项目启动时加载数据库数据到内存
  7. el-input输入值无法在输入框显示
  8. 从零开始的网站搭建,服务器与域名管理
  9. 【PTA】520 钻石争霸赛 2021,119分
  10. AcWing基础算法课Level-2 第三讲 搜索与图论