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

Django-Channels实时通知:单点赞触发多通知及WebSocket断开问题

Django Channels WebSocket重复通知问题排查与解决

我最近用Django Channels做了个WebSocket实时通知功能——用户点赞帖子时,自动给作者发实时提醒,但碰到个糟心的问题:单次点击点赞按钮,数据库里居然生成了多条重复通知。查了日志才发现,consumers.py里的notif_like()函数被反复调用,直到WebSocket连接被强制断开,终端还弹出来“Application instance took too long to shut down and was killed”的警告。

先给大家看下关键实现代码(简化版):

核心代码片段

1. 前端点赞按钮

<a class="like-btn" id="like-btn-{{ post.pk }}" data-likes="{{ post.likes.count }}" href="{% url 'like_toggle' post.slug %}"> Like </a>

2. 前端AJAX处理

$('.like-btn').click(function (event) { 
    event.preventDefault(); 
    var this_ = $(this); 
    var url = this_.attr('href'); 
    var likesCount = parseInt(this_.attr('data-likes')) || 0; 
    $.ajax({ 
        url: url, 
        method: "GET", 
        data: {}, 
        success: function (json) { 
            // 更新点赞数等DOM操作
        }, 
        error: function (json) { 
            // 错误处理
        } 
    }); 
});

3. 后端视图函数

# urls.py
urlpatterns = [ 
    path('<slug:slug>/like/', views.post_like_toggle, name="like_toggle"), 
]

# views.py
@login_required 
def post_like_toggle(request, slug): 
    post = Post.objects.get(slug=slug) 
    user = request.user 
    if user in post.likes.all(): 
        post.likes.remove(user) 
    else: 
        post.likes.add(user) 
    user_profile = get_object_or_404(UserProfile, user=user) 
    if str(user_profile.user) != str(post.author): 
        channel_layer = get_channel_layer() 
        text_dict = { 
            # 通知相关数据
        } 
        async_to_sync(channel_layer.group_send)( 
            "like_notif", 
            { 
                "type": "notif_like", 
                "text": json.dumps(text_dict), 
            } 
        ) 
    response_data = { 
        # 返回AJAX的数据
    } 
    return JsonResponse(response_data)

4. Consumer处理逻辑

# consumers.py
class LikeNotificationConsumer(AsyncConsumer): 
    async def websocket_connect(self, event): 
        print("connect", event) 
        await self.channel_layer.group_add("like_notif", self.channel_name) 
        await self.send({ "type": "websocket.accept", }) 
    
    async def notif_like(self, event): 
        print("like_notif_send", event) 
        await self.send({ "type": "websocket.send", "text": event.get('text') }) 
        # 这里居然在每个连接实例里创建通知!
        json_dict = json.loads(event.get('text')) 
        # ... 获取用户、帖子数据 ...
        await self.create_notif(recipient, sender, verb, post, description, data) 
    
    # 数据库操作的同步转异步方法
    @database_sync_to_async 
    def create_notif(self, recipient, sender, verb, post=None, description=None, data=None): 
        return Notification.objects.create(recipient=recipient, sender=sender, post=post, verb=verb, description=description, data=data)

5. WebSocket前端代码(用ReconnectingWebSocket)

var loc = window.location 
var wsStart = 'ws://' 
if(loc.protocol == 'https:') { wsStart = 'wss://' } 
var endpoint = wsStart + loc.host + "/like_notification/" 
var likeSocket = new ReconnectingWebSocket(endpoint) 

likeSocket.onmessage = function (e) { 
    console.log("message received", e) 
    var data_dict = JSON.parse(e.data) 
    // DOM更新操作
};

问题根源分析

  1. Consumer职责错位:把创建通知的逻辑放在了Consumer里,而notif_like()是每个WebSocket连接实例都会执行一次的!如果作者开了多个标签页,或者ReconnectingWebSocket重连产生了旧连接没清理,每个连接都会跑一遍创建通知的代码,自然就重复了。

  2. 全局分组推送:用了全局的like_notif分组,所有连接的用户都会收到消息,不仅浪费资源,还会导致无关用户的Consumer也执行创建通知的逻辑(虽然你过滤了接收者,但还是多跑了代码)。

  3. ReconnectingWebSocket连接泄漏:页面关闭/刷新时没有主动关闭WebSocket,导致旧连接残留,重连后多个实例同时存在。

针对性解决方案

1. 把通知创建移到视图层(最关键)

创建通知是写数据库的操作,应该只执行一次——用户点击一次点赞,视图只会被调用一次,所以把创建逻辑移到视图里:

修改views.py:

@login_required 
def post_like_toggle(request, slug): 
    post = Post.objects.get(slug=slug) 
    user = request.user 
    is_liked = user in post.likes.all()
    
    if is_liked:
        post.likes.remove(user)
        # 取消点赞不需要发通知,直接跳过
        verb = "unliked your post"
    else:
        post.likes.add(user)
        verb = "liked your post"
        # 只有点赞时才创建通知,确保只执行一次
        user_profile = get_object_or_404(UserProfile, user=user)
        if user != post.author:
            # 先创建通知
            Notification.objects.create(
                recipient=post.author,
                sender=user,
                post=post,
                verb=verb
            )
            # 再推送通知到WebSocket
            channel_layer = get_channel_layer()
            text_dict = {
                "recipient_username": post.author.username,
                "sender_username": user.username,
                "post_pk": post.pk,
                "verb": verb
            }
            async_to_sync(channel_layer.group_send)(
                f"user_notif_{post.author.id}",  # 改成用户专属分组
                {
                    "type": "notif_like",
                    "text": json.dumps(text_dict),
                }
            )
    
    response_data = {
        "liked": not is_liked,
        "likes_count": post.likes.count()
    }
    return JsonResponse(response_data)

然后修改consumers.py,只保留推送功能:

class LikeNotificationConsumer(AsyncConsumer): 
    async def websocket_connect(self, event): 
        print("connect", event)
        # 用户只加入自己的专属分组
        user_id = self.scope['user'].id
        group_name = f"user_notif_{user_id}"
        await self.channel_layer.group_add(group_name, self.channel_name)
        await self.send({ "type": "websocket.accept", }) 
    
    async def websocket_disconnect(self, event): 
        print("disconnect", event)
        user_id = self.scope['user'].id
        group_name = f"user_notif_{user_id}"
        await self.channel_layer.group_discard(group_name, self.channel_name)
    
    async def notif_like(self, event): 
        print("like_notif_send", event) 
        # 只负责把消息推送给前端,不再碰数据库
        await self.send({ "type": "websocket.send", "text": event.get('text') }) 

2. 修复WebSocket连接泄漏

给页面添加关闭事件,确保WebSocket正常断开:

window.addEventListener('beforeunload', function() {
    if (likeSocket) {
        likeSocket.close(1000, "Page unloaded");
    }
});

// 同时配置ReconnectingWebSocket的重连参数,避免频繁重连
var likeSocket = new ReconnectingWebSocket(endpoint, null, {
    reconnectInterval: 3000,
    maxReconnectAttempts: 10
});

3. 数据库层面兜底防重复

给Notification模型加唯一约束,从根源杜绝重复:

class Notification(models.Model):
    recipient = models.ForeignKey(User, blank=False, on_delete=models.CASCADE, related_name="Recipient")
    sender = models.ForeignKey(User, blank=False, on_delete=models.CASCADE, related_name="Sender")
    post = models.ForeignKey(Post, blank=True, default=None, on_delete=models.CASCADE, related_name="Post")
    verb = models.CharField(max_length=255)
    # 其他字段...
    
    class Meta:
        # 确保同一用户对同一帖子的同一操作只生成一条通知
        unique_constraints = [
            models.UniqueConstraint(fields=['recipient', 'sender', 'post', 'verb'], name='unique_like_notification')
        ]

验证效果

  1. 单次点击点赞,数据库只生成一条通知
  2. 作者开多个标签页,每个标签页都能收到实时通知,但数据库不会重复
  3. 关闭页面后,WebSocket正常断开,没有残留连接导致的重复调用
  4. 终端不再出现连接超时被杀死的警告

内容的提问来源于stack exchange,提问作者Kshitiz Srivastava

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 08:42:20