我目前正在开发一款游戏,用户可以在其中获得经验值。我的(自定义)用户模型看起来像这样:
class TriariadUser(AbstractBaseUser, PermissionsMixin):
pseudonym = models.CharField(max_length=40)
level = models.PositiveSmallIntegerField(default=1)
experience_points = models.PositiveIntegerField(default=0)
def add_ep_points(self, points):
self.experience_points += points
if self.experience_points >= get_next_level_ep(self.level):
level += 1
self.save()
现在我有各种信号监听器,可以为用户增加经验值。问题是:如果在一个请求期间出现多个 XP 增益,最后一个 XP 增益将覆盖所有其他 XP。
显然这是一个竞争条件,所以我尝试将我的函数修改为静态并使用 select_for_update:
@staticmethod
def add_ep_points(user_id, points):
user = TriariadUser.objects.select_for_update.get(pk=user_id)
user.experience_points += points
...
这按预期工作,但是模板中的用户对象没有更新。也就是说,必须发出新的请求让用户看到发生了什么。使用 django 调试工具栏我可以看到,加载用户的请求是在开始时发出的。之后进行所有相关更新。但是之后用户没有重新加载,所以显示旧状态。
我可以想到各种解决方法,例如使用 JavaScript 重新加载,但必须有一些其他解决方案(至少我希望如此)。
有没有办法在 Django 中锁定一个对象?有没有办法告诉一个对象需要重新加载?有没有更好的方法来完成这个(也许使用某种中间件?)。
请您参考如下方法:
为了避免这种情况,我会这样做。首先创建一个 UserExpRecord 模型,其中包含与用户的关系以及您要添加或删除的 xp 的 +/- 数量。然后这些信号可以添加一个新的 UserExpRecord 给用户 xp。让 UserExpRecord 发出保存信号,通知用户模型它需要重新收集 (SUM) 与用户相关的所有 xp 记录并将该值保存给用户。
这为您提供了记录何时以及为用户添加了多少 xp 的直接好处。第二个好处是您可以避免任何类型的竞争条件,因为您不会尝试锁定表行并增加值。
也就是说,根据您的后端,可能会有一个原子线程安全的“upsert”或“increment”函数,允许您在事务内安全地递增一个值,同时阻止所有其他写入。这将允许写入正确堆叠。我相信第一个解决方案(单独的 xp 记录)会比这个或您当前的解决方案(未知的竞争条件丢失/丢弃 xp 更新)带来更小的麻烦(缓存失效)。
