面试官:群聊消息的已读未读功能,你来设计一个?

本文讨论了如何设计企业IM群聊中的已读未读功能,从最初的简单方案(存储readids和unreadids)到使用bitmap优化,通过分析内存和磁盘空间占用,提出了在成员退出和重新加入时的处理策略,并介绍了如何在MySQL和Redis中实现这一方案,以降低存储成本并提高效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

企业IM比如企业微信、钉钉里面的群消息的有个已读未读的功能,发送者刚发出消息时,当前群里其他群成员都是未读状态,陆陆续续有人看了这个消息,这时候消息的详情变成x人已读,y人未读,如下图所示,有具体的已读未读列表(万恶的功能,看到同事or老板的消息不能假装没看到了),每条消息对应一个唯一的messageid(uint64_t),每个用户对应一个唯一的userid(uint64_t),应该如何保存这个消息对应的已读未读详情呢?

在这里插入图片描述

第一反应就是很简单粗暴的方案

对于每一个messageid,存当前readids + unreadids,当群成员A已读某一条消息时,把A userid从unreadids移除写到readids上就好了,客户端更新到messageid对应的详情列表,就可以展示m人已读,n人未读

显然这么简单粗暴的方案面试官是不会满意的,追问有没有更好的方案呢?
仔细分析,按照目前的设计,每一条消息,已读未读详情就要占用8B * 群成员数的内存,如果一个活跃的200人大群,每发一条消息,已读未读就要1600B,如果平均每天消息量是1k,那每个这样的群,每天就要1.6MB磁盘空间,对于客户端来说,特别是手机端,占用磁盘空间是用户不能接受的,又不能把工作消息删了,对于服务器端来说,用户群体如果特别大,那数据库存

<think>好的,我现在需要帮助用户实现WebSocket消息的已未读功能。首先,我得理解用户的需求。用户可能正在开发一个实时通讯应用,比如聊天室或者即时消息系统,需要跟踪哪些消息已经被用户阅,哪些还没有。这功能对于用户体验很重要,尤其是在群聊或者有多个对话的场景中。 接下来,我得回忆一下WebSocket的基本机制。WebSocket提供了全双工通信,适合实时应用。但WebSocket本身并不处理消息状态,所以需要自己设计实现。用户提到的已未读状态,可能需要结合客户端和服务端的协作。 根据用户提供的引用资料,特别是引用[3],里面提到使用status参数来区分消息状态。服务器返回消息列表时,status为1表示已,0未读。当用户查看消息时,客户端需要发送已回执给服务器,更新状态。这应该是核心思路。 我需要考虑几个方面: 1. **消息结构设计**:消息对象需要包含状态字段,比如status或者readStatus,可能还需要时间戳、发送者、接收者等信息。例如: $$ \text{消息对象} = \{ \text{id: string, content: string, sender: string, receiver: string, status: 0|1, timestamp: number} \} $$ 这样设计可以在服务端存储每个消息的状态。 2. **已回执机制**:当用户打开聊天窗口或滚动到消息位置时,客户端发送已确认。例如,发送一个包含消息ID和用户ID的WebSocket消息到服务器,服务器更新这些消息的状态为已。引用[3]中的例子是通过currentMessage设置status为0,可能是在进入聊天时标记为已。 3. **未读计数**:客户端需要根据消息状态统计未读数量。服务端在用户登录或重连时推送未读消息列表。这需要维护每个用户的消息队列,并过滤status为0的消息。例如,用户A登录后,服务端查询所有receiver是A且status=0的消息,推送给客户端,并更新未读计数。 4. **数据存储**:消息状态需要持久化存储,所以服务端可能需要使用数据库,比如MongoDB或MySQL。每次收到已回执,更新数据库中对应消息的状态。引用[1]提到使用RabbitMQ搭建,可能涉及消息队列处理,但这里可能直接用数据库更简单。 5. **实时同步**:当一条消息被标记为已,其他在线用户是否需要实时更新?比如在群聊中,当某个用户阅消息,其他人是否能看到该消息的状态变化。这可能需要服务端主动推送状态更新给所有相关客户端。 6. **离线处理**:用户离线时收到的消息,在下次上线时需要拉取未读消息。服务端需要记录用户的最后在线时间,或者在用户登录时查询所有未读消息。 可能的挑战包括如何高效地更新大量消息的状态,比如用户快速滚动浏览历史消息时,如何批量发送已回执,避免频繁的网络请求。另外,处理并发情况,比如多个设备同时标记同一条消息为已,需要保证数据一致性。 参考引用[2]和[3],使用Node.js和WebSocket实现的话,可以在服务端维护一个消息存储,当客户端发送已指令时,更新对应的消息状态。例如,客户端发送: ```json { "type": "mark_as_read", "messageIds": ["msg1", "msg2"], "userId": "user123" } ``` 服务端收到后,在数据库中将这些消息的status字段更新为1,并可能广播给其他相关客户端(如果需要)。 此外,未读计数的维护可以在客户端本地进行,每次接收到新消息时,如果当前聊天窗口是打开的,则自动标记为已,否则增加未读计数。当用户切换到该聊天时,发送已回执并更新计数。 需要测试不同场景,比如网络延迟、消息重复发送、客户端异常断开等情况,确保状态同步的准确性。可能还需要考虑消息的时序,避免因消息到达顺序问题导致状态错误。 总结下来,步骤大致是: - 设计消息结构,包含状态字段。 - 客户端在适当的时候发送已回执。 - 服务端更新数据库并同步状态。 - 客户端根据状态显示未读计数。 - 处理离线和多设备的情况。</think>### 分步实现WebSocket消息/未读功能 #### 1. 消息结构设计(服务端) 在数据库和通信协议中定义消息必须包含状态标识: ```javascript { id: "消息唯一ID", content: "消息内容", sender: "发送者ID", receiver: "接收者ID/群ID", status: 0, // 0未读 1已 timestamp: 1672531200000 } ``` 使用MongoDB时可创建消息集合: ```javascript db.messages.createIndex({ receiver: 1, status: 1 }) // 加速查询 ``` #### 2. 未读消息推送(服务端) 用户建立WebSocket连接时推送未读消息: ```python # Python伪代码示例 async def on_connect(user_id): unread = db.messages.find({ "receiver": user_id, "status": 0 }) await websocket.send(json.dumps({ "type": "initial_unread", "data": list(unread) })) ``` #### 3. 实时消息处理流程 ```mermaid sequenceDiagram participant A as 客户端A participant S as 服务端 participant B as 客户端B A->>S: 发送消息{id:123,status:0} S->>B: 推送新消息(带未读状态) S->>S: 持久化到数据库 B->>S:后发送已回执{msgId:123} S->>S: 更新消息状态为已 S->>A: 推送状态更新通知 ``` #### 4. 已回执实现(客户端) ```javascript // 监听可视区域变化 const observer = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting) { const msgId = entry.target.dataset.id; ws.send(JSON.stringify({ type: 'mark_read', messageIds: [msgId] })); } }); }); // 绑定消息DOM元素 document.querySelectorAll('.message').forEach(el => { observer.observe(el); }); ``` #### 5. 未读计数优化方案 $$ \text{未读数} = \sum_{i=1}^{n} \begin{cases} 1 & \text{当 } status_i=0 \text{ 且 } \neg isActiveChat \\ 0 & \text{其他情况} \end{cases} $$ 使用Redis维护实时计数器: ```python def update_unread_count(user_id, delta): redis.incrby(f'unread:{user_id}', delta) # 设置过期时间防止内存泄漏 redis.expire(f'unread:{user_id}', 86400) ``` #### 6. 异常情况处理 - 消息去重:为每条消息添加UUID - 离线补偿:记录最后接收消息时间戳 - 状态同步:定期全量同步(每日1次) ```javascript // 客户端重连时检查 ws.addEventListener('open', () => { if (lastReceivedTime < Date.now() - 60000) { requestFullSync(); } }); ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值