聊天系统设计
怎么保证实时性?为什么不用http/2和grpc?√
使用websocket
http/2优点:多路复用,头部压缩,流量控制和优先级;延时高,实时性差,设计初衷以请求响应为主
grpc优点:高效的二进制协议、多语言支持、内置负载平衡、重试机制和超时控制,认证等;延时低,实时性差,微信小程序不直接支持
WebSocket优点:全双工通信,低延迟:广泛支持:W3C标准,浏览器兼容性好。协议简单,适合于频繁且快速的小消息传输,如聊天消息
mqtt:通常基于TCP,微信小程序的网络接口(wx.request和connectSocket)无法直接操作TCP,需要适配
为什么消息无序?怎么保证消息有序?怎么生成消息id?怎怎么保证幂等?√么做消息已读?用户在线?
websocket本身有序
应用层的并发处理:服务端或者客户端收到消息,使用异步任务解析,存储或展示消息
网络延迟和分布式系统的时钟问题
WebSocket重连后的消息重发
在消息中附加唯一序列号分布式ID(时间戳+机器id+机器seq)
服务器和客户端统一时间标准(如 NTP)。
客户端对消息进行排序后再展示。
在重连时请求消息历史,并按序补齐。
前端根据发送requestId(uuid),用Redis缓存校验是否已经处理过,后端生成分布式ID,
前端生成唯一id,后端用唯一id判断幂等,服务端再生成id返回服务端消息id和客户端消息id
记录每个人对群或者对人已读的最大消息ID,根据最大消息ID判断是否已读
用户每次登陆或者退出通知后台,后台根据uid使用bitmap存
读扩散vs写扩散
读扩散
A与每个聊天的人跟群都有一个信箱Timeline,A查看聊天信息时读取所有有新消息的信箱。
写操作轻量,单聊\群聊都只要写一次相应的信箱
每个信箱保存两个人的聊天记录,查看和搜索方便
读操作很重
写扩散
每个人都只从自己的信箱里读取消息
单聊:往自己的信箱跟对方的信箱都写消息,如果要查看两人聊天历史记录的话还要再写一份(从个人信箱也能回溯出两人的聊天历史记录,但效率很低)
群聊:往所有成员的信箱写消息,同时,如果要查看群的聊天历史记录的话还要再写一份
读操作轻量。方便消息的多端同步
写操作重,对于群聊来说
整体设计
Netty集群 + Nginx + zookeeper:客户端接入、长连接管理、消息转发、查询目标在线状态
SpringCloudAlibaba集群:核心业务逻辑、服务治理、分布式事务、 RabbitMQ 进行消息解耦和异步处理存储消息
Redis集群:在线状态管理、消息缓存、未读计数
RabbitMQ镜像集群:消息队列解耦、离线消息存储、群聊消息广播
MySQL集群 + ProxySQL:用户信息、好友关系、消息索引存储
MongoDB集群Sharding:聊天记录存储、离线消息存储、消息检索
protobuf消息+json短链接交互协议:处理消息和非消息业务
单聊消息流程
客户端直连Netty集群发送消息
Netty通过Redis查询用户是否在线,不经过springCloud
在线则直接转发给目标客户端,然后调用springcloud保存到mysql
不在线则转发到springCloud用rabbitmq保存离线消息到MongoDB,
用户上线时会通过Netty调用springcloud查询MongoDB的离线消息,然后调用springcloud保存到mysql
群聊消息流程
客户端直连Netty集发送群消息
Netty通过springcloud获得群成员列表,通过redis查询群员状态,在线则直接转发给目标客户端,前端确认收到后调用SpringCloud保存MysSQL
Netty将不在线的成员的消息转发给RabbitMQ
RabbitMQ使用Fanout Exchange,将消息广播群成员的消费队列
SpringCloud消费消息,用rabbitmq保存离线消息到MongoDB
~
netty和SpringCloud隔离原因,
长连接可以减少短连接连接频繁创建和断开的消耗,都需要三次握手和四次挥手,
避免长短连接争抢连接资源。
扩展性好,每个集群单独扩展互不影响
netty服务器x与netty服务器y之间需要路由和转发,路由结构是网状结构,转发可以转发到netty客户端或者另外一个netty服务器上
一个服务器转发一万个人时有专门netty客户端做转发,只需和专门转发的netty客户端建立长连接,而netty客户端会代理转发。
为什么选protobuf协议?
压缩效率高,消息体积小,传输速度快,延迟低;支持跨语言;支持嵌套复杂数据结构