如何让Django视图临时成为单例?解决重复点击超额加钱问题
解决Django视图钱包并发超额问题
这个问题是典型的并发竞态条件:当用户快速重复点击按钮时,多个请求会同时读取到用户钱包的旧余额,各自计算需要添加的金额后执行保存操作,最终导致钱包余额超过10美元的上限。不用修改模型或引入外部包,仅通过调整视图逻辑就能解决,核心是用Django的行级锁来避免并发冲突。
函数视图修复方案
我们需要利用select_for_update锁定用户数据行,确保同一时间只有一个请求能修改用户余额,同时在事务内完成操作保证原子性:
from django.db import transaction from django.contrib.auth.models import User from django.http import HttpResponse def my_view(request): # 开启原子事务,确保锁定和修改操作的完整性 with transaction.atomic(): # 重新查询并锁定当前用户对象,避免使用request.user(已缓存旧数据) user = User.objects.select_for_update().get(id=request.user.id) # 计算需要添加的金额(基于锁定后的最新余额) money_needed = 10 - user.money if money_needed > 0: # 直接一次性加到上限,替代原有的循环累加(更高效且减少冲突) user.money += money_needed user.save() return HttpResponse("Ok!")
关键修复点说明
select_for_update():会在数据库层面锁定查询到的用户行,其他请求必须等待当前事务提交后才能读取该用户数据,彻底避免并发读取旧值的问题。transaction.atomic():包裹整个操作,确保锁定和修改是原子性的,要么全部完成,要么全部回滚,避免中间状态引发的异常。- 重新查询用户:不能直接使用
request.user,因为它是请求初始化时缓存的旧数据,没有被锁定,必须通过带锁的查询获取最新的用户对象。 - 移除循环累加:原代码的循环每次加1都执行
save(),既低效又增加了冲突概率,直接一次性加到上限更合理。
类视图替代方案
如果更倾向于使用类视图,可以基于UpdateView实现,同样利用行级锁:
from django.db import transaction from django.contrib.auth.mixins import LoginRequiredMixin from django.views.generic.edit import UpdateView from django.contrib.auth.models import User from django.http import HttpResponseRedirect class WalletTopUpView(LoginRequiredMixin, UpdateView): model = User fields = ['money'] def get_object(self, queryset=None): # 在获取对象时锁定用户行 with transaction.atomic(): return User.objects.select_for_update().get(id=self.request.user.id) def form_valid(self, form): user = self.get_object() money_needed = 10 - user.money if money_needed > 0: user.money += money_needed user.save() # 重定向回原页面(或其他指定页面) return HttpResponseRedirect(self.request.path)
内容的提问来源于stack exchange,提问作者Manuel Fedele
相关产品推荐
相关产品推荐

