如何为宝藏页面实现单用户锁定以避免重复分配问题?
这个问题我之前也碰到过——本质就是多用户并发操作时的竞态条件:当两个用户同时触发Next按钮,数据库还没来得及把第一个用户选中的宝藏标记为「已捕获」,第二个用户的查询就已经拿到了同一条结果,最终导致两人跳转到同一个页面。
下面是两种能完美解决问题的方案,都能实现你要的「锁定」机制,同时保留按ID排序的需求:
方案1:数据库原子事务 + 排他锁(最直接推荐)
Django提供的select_for_update()可以在查询时给数据库行加排他锁,确保同一时间只有一个用户能拿到这条宝藏记录,直到我们完成标记操作。修改你的视图函数如下:
from django.db import transaction from django.contrib.auth.decorators import login_required @login_required # 确保只有登录用户能操作 def get_next_available_treasure(request, current_treasure_id): """Retrieve the next unclaimed treasure with atomic locking to prevent race conditions.""" with transaction.atomic(): # 加排他锁,其他事务必须等当前事务结束才能访问这条记录 treasure = Treasure.objects.filter( id__gt=current_treasure_id, captured_by__isnull=True ).order_by('id').select_for_update().first() if not treasure: # 没有可用宝藏时的跳转逻辑,比如返回提示页 return redirect('no-more-treasures') # 立即标记为当前用户所有 treasure.captured_by = request.user treasure.save() return redirect('treasure-page', treasure_id=treasure.id)
为什么这个方案有效:
transaction.atomic()把查询和更新打包成一个原子操作,要么全部完成,要么全部回滚,中间不会被其他事务打断。select_for_update()会锁定查到的第一条未被捕获的宝藏,其他用户的查询会自动等待当前事务结束,之后他们查到的就是下一条可用宝藏了。- 一定要在事务块内完成查询和更新,不然锁会提前释放,还是会出现竞态问题。
方案2:乐观锁(适合高并发场景)
如果你的应用有极高的并发量,不想因为锁表影响性能,可以用乐观锁的方式——通过版本号来判断记录是否被其他用户修改过,避免长时间锁表。
首先给Treasure模型加一个版本号字段:
from django.db.models import F class Treasure(models.Model): # 你的其他字段... captured_by = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL) version = models.IntegerField(default=0) # 新增版本号字段
然后修改视图函数:
from django.db import transaction, IntegrityError from django.contrib.auth.decorators import login_required @login_required def get_next_available_treasure(request, current_treasure_id): while True: # 先查询下一条可用宝藏(不加锁) treasure = Treasure.objects.filter( id__gt=current_treasure_id, captured_by__isnull=True ).order_by('id').first() if not treasure: return redirect('no-more-treasures') try: with transaction.atomic(): # 只有当版本号和查询时一致、且仍未被捕获时,才更新 updated_rows = Treasure.objects.filter( id=treasure.id, version=treasure.version, captured_by__isnull=True ).update( captured_by=request.user, version=F('version') + 1 # 更新版本号 ) if updated_rows == 1: # 更新成功,说明没被其他用户抢,跳转 return redirect('treasure-page', treasure_id=treasure.id) # 更新失败,说明被其他用户抢了,循环重新查询 except IntegrityError: # 并发冲突,继续循环重试 continue
这个方案的优势:
- 不会长时间锁定数据库行,适合高并发场景。
- 通过版本号验证,确保只有第一个发起更新的用户能成功标记宝藏,其他用户会自动重试查询下一条。
额外注意点
- 一定要处理
treasure为None的情况,也就是没有更多可用宝藏时,避免抛出AttributeError。 - 必须确保用户已登录,所以加上
@login_required装饰器,防止匿名用户操作。 - 测试时可以打开两个浏览器窗口,同时点击Next按钮,验证是否会跳转到不同的宝藏页面。
内容的提问来源于stack exchange,提问作者Kurt Bourbaki
相关产品推荐
相关产品推荐

