来源:https://www.toutiao.com/i6686...我和一个朋友讨论了一个他前段时间面试大公司的话题:企业IM比如企业微信,在钉钉里面有个群消息已读和未读功能,当发件人刚刚发了一条消息,当前群里的其他群成员都未读,大家陆续阅读了这条消息,此时消息详情变为x人已读,x人未读ypeopleRead,如下图,有一个特定的已读和未读列表(一个邪恶的功能,你不能假装没有看到同事或老板的消息),每条消息对应一个唯一的messageid(uint64_t),而每个用户对应一个唯一的userid(uint64_t),如何保存这条消息对应的已读和未读详情呢?我马上给出了一个非常简单粗暴的解决方案:对于每一个messageid,保存当前的readids+unreadids,当群成员A读过某条消息后,将Auserid从unreadids中取出写入readids即可。客户端更新messageid对应的详情列表,可以显示m人已读,n人未读。很显然,这样简单粗暴的解决方案,面试官是不会满意的。请问有没有更好的解决办法?仔细分析,按照目前的设计,每条消息的已读和未读详情会占用8B*群成员数。平均每日消息量为1k,因此每个这样的组每天需要1.6MB的磁盘空间。对于客户端,尤其是手机,占用的磁盘空间是用户无法接受的,工作消息也无法删除。对于服务器端来说,如果用户群体特别大,数据库存储的成本是不小的。事实上,未读和已读只是一个0/1标记。可以维护位图来实现这一点吗?我应该怎么办?组元信息保存userid到自增mapid的映射structUserInfo{uint64_tuserid;uint32_tmapid;};structGroupMetaInfo{vector成员;字符串名称;uint32_t最大标识符;//其他信息};中,有mapid<->usreid的双向映射。如果组内有5个成员ABCDE,则对应的mapid为1-5,messageid对应的消息详情存储可以设计为{uint32_tmaxid,uint8_treadbit[]}比如上面的情况就是{5,readbit[0]=bin(00000000)};占用5B(4+1),当A发送消息,D读取消息后,更新为{5,readbit[0]=bin(00001000)},更新为{5,readbit[0]=bin(00011110)}当其他4人阅读消息时值得思考:退出的成员怎么办?比如C退群,发消息时maxid还是5,已读+未读的总人数应该是3(不包括发消息的人)。目前,该信息只有5位(0/1),无法识别谁离开了群组。成员聊完退出群怎么办?从GruopMetaInfo中删除它?成员退出群聊重新加入时如何分配id?首先,在第2点,退出群聊的成员只能标记为删除,不能物理删除。否则,当客户端显示已读和未读详情时,通过mapid找不到对应的userid,退出的成员重新加入群聊。搞定,把标记删除改成非标记删除,还是把旧的mapid改成1?我目前想到的更好的办法是在发送消息时再添加一个位图来记录成员是否退出群聊,退出群聊后设置为1,所以最终的解决方案是在群中添加userid信息和自增mapid双向映射,退出群聊成员标记删除,messageid已读和未读详情存储{maxid,readbit[],quitbit[]}新方案会带来什么样的好处?增加自增mapid字段,维持群聊,成本几乎可以忽略不计。每个成员的已读和未读从8B(64bit)优化为2bit,减少62/64,200个已读和未读。老方案是1600B,现在只需要(200/8)*2+4=54,每条消息节省95%+如果maxid达到百万甚至千万,岂不是一场灾难?一般实际场景下,群聊都会限制人数。即使不断踢人,不断加新人,maxid也只能达到企业的人数。位图存储成本远超原方案,可以原方案实现,客户端可以提前埋掉兼容逻辑。近期热点文章推荐:1.1000+Java面试题及答案(2022最新版)2.厉害了!Java协程来了。..3.SpringBoot2.x教程,太全面了!4、SpringBoot2.6正式发布,一大波新特性。.5.《Java开发手册(嵩山版)》最新发布,赶快下载吧!感觉不错,别忘了点赞+转发!