44

这是一个具有潜在竞争条件的 django 视图的简单示例:

# myapp/views.py
from django.contrib.auth.models import User
from my_libs import calculate_points

def add_points(request):
    user = request.user
    user.points += calculate_points(user)
    user.save()

竞争条件应该相当明显:用户可以发出两次此请求,并且应用程序可能user = request.user同时执行,导致其中一个请求覆盖另一个请求。

假设函数calculate_points比较复杂,根据各种奇葩的东西进行计算,不能放在一个单一的update,也很难放在存储过程中。

所以这是我的问题:django 可以使用什么样的锁定机制来处理类似的情况?

4

6 回答 6

49

Django 1.4+ 支持select_for_update,在早期版本中,您可以执行原始 SQL 查询select ... for update,例如根据底层数据库将锁定该行的任何更新,您可以对该行执行任何操作,直到事务结束。例如

from django.db import transaction

@transaction.commit_manually()
def add_points(request):
    user = User.objects.select_for_update().get(id=request.user.id)
    # you can go back at this point if something is not right 
    if user.points > 1000:
        # too many points
        return
    user.points += calculate_points(user)
    user.save()
    transaction.commit()
于 2012-06-11T20:41:10.597 回答
21

从 Django 1.1 开始,您可以使用 ORM 的 F() 表达式来解决这个特定问题。

from django.db.models import F

user = request.user
user.points  = F('points') + calculate_points(user)
user.save()

有关更多详细信息,请参阅文档:

https://docs.djangoproject.com/en/1.8/ref/models/instances/#updating-attributes-based-on-existing-fields

https://docs.djangoproject.com/en/1.8/ref/models/expressions/#django.db.models.F

于 2009-12-23T22:40:34.450 回答
8

数据库锁定是这里的方法。有计划向 Django 添加“选择更新”支持(此处),但目前最简单的方法是在开始计算分数之前使用原始 SQL 更新用户对象。


当底层数据库(例如 Postgres)支持悲观锁定时,Django 1.4 的 ORM 现在支持它。请参阅Django 1.4a1 发行说明

于 2009-06-23T03:09:14.483 回答
7

你有很多方法可以单线程这种事情。

一种标准方法是首先更新。您执行更新,将获取行上的排他锁;然后做你的工作;最后提交更改。为此,您需要绕过 ORM 的缓存。

另一种标准方法是使用单独的单线程应用程序服务器,将 Web 事务与复杂计算隔离开来。

  • 您的 Web 应用程序可以创建一个评分请求队列,生成一个单独的进程,然后将评分请求写入此队列。spawn 可以放在 Django 中urls.py,所以它发生在 web-app 启动时。或者可以将其放入单独的manage.py管理脚本中。或者可以在尝试第一个评分请求时“根据需要”完成。

  • 您还可以使用 Werkzeug 创建一个单独的 WSGI 风格的 Web 服务器,该服务器通过 urllib2 接受 WS 请求。如果此服务器只有一个端口号,请求将按 TCP/IP 排队。如果您的 WSGI 处理程序有一个线程,那么您已经实现了序列化单线程。这稍微更具可扩展性,因为评分引擎是一个 WS 请求并且可以在任何地方运行。

另一种方法是拥有一些必须获取和持有的其他资源来进行计算。

  • 数据库中的 Singleton 对象。可以使用会话 ID 更新唯一表中的单行以获取控制权;使用会话 ID 更新None以释放控制。基本更新必须包含一个WHERE SESSION_ID IS NONE过滤器,以确保在其他人持有锁时更新失败。这很有趣,因为它本质上是无竞争的——它是一个单一的更新——而不是一个 SELECT-UPDATE 序列。

  • 一个普通的信号量可以在数据库之外使用。队列(通常)比低级信号量更容易使用。

于 2009-06-23T02:28:49.230 回答
1

这可能过于简单化了您的情况,但是仅仅替换一个 JavaScript 链接呢?换句话说,当用户单击链接或按钮时,将请求包装在 JavaScript 函数中,该函数会立即禁用/“灰显”链接并将文本替换为“正在加载...”或“提交请求...”信息或其他内容相似的。这对你有用吗?

于 2009-06-23T03:15:06.470 回答
0

现在,您必须使用:

Model.objects.select_for_update().get(foo=bar)
于 2014-06-24T08:00:19.420 回答