以下是具有潜在竞争条件的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可以使用哪种锁定机制来处理类似的情况?
答案 0 :(得分:44)
Django 1.4+支持select_for_update,在早期版本中,您可以执行原始SQL查询,例如select ... for update
取决于底层数据库将锁定任何更新的行,您可以使用该行执行任何操作,直到事务结束。 e.g。
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()
答案 1 :(得分:17)
从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/expressions/#django.db.models.F
答案 2 :(得分:8)
数据库锁定是这里的方法。有计划为Django(here)添加“select for update”支持,但目前最简单的方法是在开始计算分数之前使用原始SQL来更新用户对象。
当底层数据库(如Postgres)支持它时,Django 1.4的ORM现在支持悲观锁定。请参阅Django 1.4a1 release notes。
答案 3 :(得分:6)
你有很多方法可以单线程处理这种事情。
一种标准方法是更新第一。你做了一个更新,它将占用该行的独占锁;然后做你的工作;并最终提交更改。为此,您需要绕过ORM的缓存。
另一种标准方法是使用单独的单线程应用程序服务器,将Web事务与复杂计算隔离开来。
您的Web应用程序可以创建评分请求队列,生成单独的进程,然后将评分请求写入此队列。产卵可以放在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序列。
可以在数据库外部使用花园种类的信号量。队列(通常)比低级信号量更容易使用。
答案 4 :(得分:1)
这可能会使您的情况过于简单,但仅仅更换JavaScript链接呢?换句话说,当用户点击链接或按钮将请求包装在JavaScript函数中时,该函数会立即禁用/“灰显”链接并将文本替换为“正在加载...”或“提交请求...”信息或其他内容类似。这对你有用吗?
答案 5 :(得分:0)
现在,你必须使用:
Model.objects.select_for_update().get(foo=bar)