Django Channels在线井字棋用户胜负分存储后被重置问题求助
问题排查与解决方案
核心问题原因
你遇到的分数被重置问题根源是Django模型实例的旧值覆盖:
- 你在
consumers.py中使用的self.scope['user']是WebSocket连接建立时加载的用户实例,后续数据库中用户的胜场数据更新后,这个缓存的实例里的won_games/lost_games/draw_games字段还是连接建立时的初始值(比如0) - 当你后续调用缓存实例的
lose()/win()方法时,修改某一个字段后执行全字段保存,会把其他字段的旧值直接覆盖数据库中的最新值,就会出现赢了的场次被重置为0的情况。 - 同理通过
presence_set关联查询到的competitor.user也是ORM缓存的旧实例,也会出现同样的覆盖问题。
修复方案
1. 优化模型方法,使用原子更新避免旧值覆盖
用Django的F表达式直接在数据库层面执行数值累加,不需要把字段值读到Python内存中,同时指定update_fields仅更新需要修改的字段,避免其他字段被旧值覆盖:
from django.db.models import F class CustomUser(AbstractUser): won_games = models.IntegerField(default=0) lost_games = models.IntegerField(default=0) draw_games = models.IntegerField(default=0) def win(self): self.won_games = F('won_games') + 1 self.save(update_fields=['won_games']) def lose(self): self.lost_games = F('lost_games') + 1 self.save(update_fields=['lost_games']) def draw(self): self.draw_games = F('draw_games') + 1 self.save(update_fields=['draw_games'])
2. 优化Consumer逻辑,每次更新前拉取最新的用户实例
不要直接用缓存的用户实例操作,每次统计胜负前从数据库查询最新的用户数据:
class GameConsumer(WebsocketConsumer): # some code def receive(self, text_data): if text_data['type'] == 'completed': self_channel = Presence.objects.get(channel_name=self.channel_name) competitor_presence = self_channel.room.presence_set.filter( ~Q(channel_name=self_channel) ).first() if not competitor_presence: return # 重新查询最新的用户实例,不要用缓存的关联对象 competitor = CustomUser.objects.get(pk=competitor_presence.user.pk) user = CustomUser.objects.get(pk=self.scope['user'].pk) competitor.win() if user.is_authenticated: user.lose() return
可选优化:高并发场景加行锁
如果同时有多个游戏局更新同一个用户的分数,可以加select_for_update避免竞态条件:
from django.db import transaction with transaction.atomic(): competitor = CustomUser.objects.select_for_update().get(pk=competitor_presence.user.pk) user = CustomUser.objects.select_for_update().get(pk=self.scope['user'].pk) competitor.win() user.lose()
内容的提问来源于stack exchange,提问作者Ahmed Yasser
相关产品推荐
相关产品推荐

