✨ 记一次实时聊天消息重复显示 BUG 的修复
🐛 问题现象
项目中的实时聊天功能出现了一个诡异的 BUG:
用户发送消息后,发送方视角会看到两条相同的消息。但刷新页面后,又恢复正常只显示一条。
🔍 这个问题只在发送方出现,其他用户看到的都是正常的单条消息。
🏗️ 技术架构
聊天功能采用的技术方案:
| 模块 | 技术 |
|---|---|
| 前端 | React + Next.js |
| 实时通信 | SSE (Server-Sent Events) |
| 后端 | Next.js API Routes + Prisma ORM |
📨 消息流转流程
graph LR
A[用户发送消息] --> B[POST API 保存到数据库]
B --> C[后端 broadcastMessage 通过 SSE 广播]
C --> D[前端 SSE 监听器接收广播]
D --> E[更新 UI]
🔬 根本原因分析
经过代码审查,发现问题出在消息被添加了两次:
📍 第一次添加:POST 响应
// handleSend() 中,POST 成功后立即添加(乐观更新)
const data = await res.json()
if (data.success) {
setInput('')
setMessages(prev => [...prev, data.data]) // ← 第一次添加
}
📍 第二次添加:SSE 广播
// useEffect 中的 SSE 监听
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data)
if (data.type === 'message') {
setMessages(prev => [...prev, data.data]) // ← 第二次添加
}
}
🤔 为什么其他人看不到重复?
| 角色 | 消息来源 | 结果 |
|---|---|---|
| 发送方 | POST 响应 ✅ + SSE 广播 ✅ | 两条消息 ❌ |
| 其他用户 | 只有 SSE 广播 | 一条消息 ✅ |
💡 因为 POST 响应只在发送方本地执行,而 SSE 广播会推送给所有订阅者(包括发送方自己)
🛠️ 解决方案
给两处添加消息的逻辑都加上 id 去重检查:
SSE 广播处理
if (data.type === 'message') {
setMessages(prev => {
if (prev.some(m => m.id === data.data.id)) return prev
return [...prev, data.data]
})
}
POST 响应处理
if (data.success) {
setInput('')
setMessages(prev => {
if (prev.some(m => m.id === data.data.id)) return prev
return [...prev, data.data]
})
}
✅ 这样无论消息先从哪个通道到达,都能保证不会重复添加。
📚 经验教训
1️⃣ 乐观更新 + 实时广播是常见陷阱
使用乐观更新提升用户体验时,如果后端同时会广播消息给发送者,就容易出现重复。需要在设计阶段就考虑去重机制。
2️⃣ 基于唯一标识去重是最可靠的方案
使用消息 ID 去重比使用时间戳或内容比对更可靠,因为 ID 是唯一的,而内容可能相同。
3️⃣ 测试时要关注发送方视角
实时消息功能的测试,不仅要验证接收方看到的消息是否正确,也要验证发送方自己看到的是否正确。
🎯 总结
这个 BUG 的本质是 乐观更新与实时广播的冲突。修复方案很简单,但找到根因需要理解整个消息流转链路。
✨ 建议:在开发实时通信功能时,在设计阶段就明确消息的添加时机和去重策略,避免类似问题。