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更新操作 };
问题根源分析
Consumer职责错位:把创建通知的逻辑放在了Consumer里,而
notif_like()是每个WebSocket连接实例都会执行一次的!如果作者开了多个标签页,或者ReconnectingWebSocket重连产生了旧连接没清理,每个连接都会跑一遍创建通知的代码,自然就重复了。全局分组推送:用了全局的
like_notif分组,所有连接的用户都会收到消息,不仅浪费资源,还会导致无关用户的Consumer也执行创建通知的逻辑(虽然你过滤了接收者,但还是多跑了代码)。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') ]
验证效果
- 单次点击点赞,数据库只生成一条通知
- 作者开多个标签页,每个标签页都能收到实时通知,但数据库不会重复
- 关闭页面后,WebSocket正常断开,没有残留连接导致的重复调用
- 终端不再出现连接超时被杀死的警告
内容的提问来源于stack exchange,提问作者Kshitiz Srivastava

