You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为宝藏页面实现单用户锁定以避免重复分配问题?

这个问题我之前也碰到过——本质就是多用户并发操作时的竞态条件:当两个用户同时触发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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.13 07:32:34