对公司IM架构的一些思考

在当今信息互联的时代,即时通讯(IM)架构成为企业通信的核心。以公司使用的Netty和Redis构建的IM架构为例,深入探讨其中涉及的功能模块、优化方案以及架构扩展等方面的思考。

1. 架构概述

1.1技术栈选择

Netty:作为通信框架,因为它高效、灵活,支持跨平台

Redis:作为数据存储,因为它提供快速缓存、数据安全、灵活数据结构。这两者的搭配使系统更快、更灵活、更可靠。

1.2 功能模块

用户:功能使用者。

消息:用户之间产生的消息记录(文本,图片,表情,文件,视频,语音通讯,业务等等)。

会话:用户之间聊天产生的联系。

群:多用户之间聊天产生的联系(群主,成员包括权限)。

终端:客户端(web,ios,Android)。

未读数:用户未读消息数。

用户状态:用户在线、离线等。

成员关系:单向好友,双向好友、关注等。

单聊:一对一。

群聊:多人互动。

客服:客服与游客或用户沟通(业务沟通咨询)。

内部业务:涉及公司内部系统通讯、调用。

2.架构优化与扩展

2.1如何保证聊天系统消息的可靠投递(不丢消息)

  1. 客户端通过Netty发送消息,每条消息带有唯一ID以应对超时或失败,IM服务端用该ID去重。
  2. 为确保消息不丢失,可以使用RocketMQ的可靠消息机制,提供持久化、容错和可靠的消息传递。
    1. 对于中间件选型思考(常用的MQ产品包括Kafka、RabbitMQ和RocketMQ)

RabbitMQ

  • 适合小到中等规模的项目,特别是对于简单的消息队列需求,比如任务分发、事件处理等。
  • 如果对消息队列没有过多的复杂需求,RabbitMQ可以选择。

RocketMQ

  • 适合大规模分布式系统,对高性能和大流量有需求的场景。
  • 特别擅长处理需要精确顺序的业务,比如有严格顺序要求的场景。
  • 支持分布式事务消息,适用于一些需要事务一致性的业务。

Kafka:

  • Kafka是一个高吞吐、分布式的消息队列系统,具有出色的持久性、可靠性和水平扩展性。
  • 适用于日志收集、消息系统、用户活动跟踪等。

如何选择:

  • 如果项目小而简单,选择 RabbitMQ 就够了。
  • 如果项目规模大,对性能和顺序性有严格要求,或者在阿里云上,可以考虑 RocketMQ。

最终,具体选择要根据项目的规模、性能需求以及团队的熟悉程度来决定。

  1. 通过客户端的ACK确认接收消息的机制来保证不丢消息

2.2离线消息服务保证IM系统的高性能

离线消息,用户不在线时收到的消息。考虑用户上下线可能频繁,通常会在用户上线时主动请求服务端的离线消息。直接从数据库拉消息可能会给数据库带来巨大压力。这里可以选择使用高性能缓存,Redis来存储这些消息,以抗住高并发的访问压力。

当然,Redis一般采用集群架构,多节点。这里可以把用户在提升几个数量级来思考。就需要考虑离线消息在Redis集群是否存储得了。为了更好控制存储空间,需要用到一些存储策略,比如仅保留最近一周或一个月的数据,并限制存储消息的条数,例如最多存储最近的1000条。既能保留必要的离线消息,又能减轻数据库压力。

在分析,历史消息很少有人会把历史消息全部看完,用户上线展示最近的一些离线消息就行。如果在极端情况需要查看更早的消息,可以从数据库查询。小概率的操作对数据库的负担是可以接受的。

2.3 海量历史聊天消息数据存储方案优化

1.发送消息处理

用户A与用户B发送一条消息,需要在消息表里存储两条消息。设计初衷是两条消息是存储在同一张表,随着数据量增大查询越来越慢。

优化:

  1. 拆分历史消息表,分为两张表。
  • 消息内容表(im_content):用户A与用户B发送一条消息存储一条记录。
  • 用户信箱表(im_user_box_table):在用户信箱索引表村粗两条记录,一条用户A发件箱,一条用户B收件箱。为什么要存储两条记录,考虑到收发两方各自删除聊天记录的情况

2.聊天记录消息处理

  • 查询用户A与用户B的聊天记录,查询im_user_box_table索引表id
sql 复制代码
CREATE TABLE `im_user_box_table` (
  `send_id` int(11) NOT NULL COMMENT '发送人id',
  `receive_id` int(11) NOT NULL COMMENT '接收人id',
  `content_id` int(11) NOT NULL COMMENT '消息id',
  `box_type` tinyint(11) NOT NULL COMMENT '信箱类型:1-发件箱,2-收件箱',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`send_id`,`content_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信箱消息索引表';
sql 复制代码
CREATE TABLE `im_content` (
  `content_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '消息id',
  `content` varchar(1000) NOT NULL COMMENT '消息内容',
  `send_id` int(11) NOT NULL COMMENT '发送人id',
  `receive_id` int(11) NOT NULL COMMENT '接收人id',
  `msg_type` tinyint(11) NOT NULL COMMENT '消息类型:1-系统消息,2-文本消息,3-语音消息,4-视频消息',
  `is_received` tinyint(11) NOT NULL DEFAULT '0' COMMENT '消息是否已被接收:0-未接收,1-已接收',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`content_id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COMMENT='消息内容表';
ini 复制代码
SELECT
	content_id,
	box_type 
FROM
	im_user_box_table t 
WHERE
	t.send_id = 1 AND 
  t.receive_id = 2 
ORDER BY content_id limit 0,10;
#之后for循环关联im_content表content_id消息内容
SELECT * from  im_content where content_id ='';
  • 还可以继续优化,因为海量的历史消息,可以考虑分库分表方案, im_user_box_table表可以按照send_id来分, 这样在聊天记录查询是不需要跨表查询的, im_content表可以按照content_id来分,因为查询消息基本都是按照消息id主键来查。

2.4redis存储离线消息

bash 复制代码
#score存储消息id
zadd offline_msg_#{receiverId} #{content_id} #{content}
bash 复制代码
#按照消息id从大到小排序取最新的十条
zrevrange offline_msg_#{receiverId} 0 9
shell 复制代码
#删除客户端已读介于最小消息id和最大消息id之间的所有消息
zremrangebyscore offline_msg_#{receiverId} min_mid max_mid

如果单个key消息存储过大,可以考虑按周或者按月针对同一个receiverId多搞几个key分段来存储

json 复制代码
ZADD offline_msg_month_#{receiverId}_2024_#{month} 1 '{"content_id":1, "msg":"wzx"}'
json 复制代码
ZADD offline_msg_week_#{receiverId}_2024_#{week} 1 '{"content_id":1, "msg":"wzx"}'

2.5群聊数据收发机制读扩散与写扩散

群设计是和单聊一起存储到历史消息表,设计过于冗余,历史消息表过大查询过慢。

优化:

1、群聊消息设计成读扩散 ,用户在群里发送一条消息只存一份数据,群里所有人读同一份消息数据,这种方式简单,但是有问题,比如实现群聊已读用户列表不太好实现。

2、写扩散 机制:用户在群里发一条消息会对群里每一个用户都存一条消息索引,单独存储一份消息内容,这样可以针对用户是否已读可以做处理,这样实现也会有问题,比如群聊人员不能过多,造成大量存储浪费。

所以根据业务可以选择哪一种机制更符合公司业务。

2.6 基于Lua脚本保证消息未读数的一致性

在公司业务迭代中出现红点问题居多,需要做到三端同步不易,文本、图片、语音电话、内部业务消息等,无法彻底解决红点同步问题,开始想法很简单操作数据库,但是做不到消息的一致性,应保证消息的原子性。

  1. 未读消息可以用redis来维护,比如用户A给用户B发送信息,需要维护两个redis key,一个是用户B总的未读数key加1,一个是用户A_用户B的未读数key加1,这两个key的加1操作,需要保证原子性,可以用lua脚本实现。
  2. 为什么要单独维护一个总的未读数,其实可以对消息会话的未读数求和就可以了,这里考虑到用户的消息会话比较多的话,会出现性能瓶颈,用户每个消息会话的未读数时刻在变化,在求和过程中,可能之前的读取的消息未读数又发生变化了,所以可以在这里单独维护一个总未读数。
  3. 对于群聊的未读数,可以针对群里每个人维护一个未读数key,比如用hash结构来存储,一个群里的所有用户的未读数可以用一个
shell 复制代码
#gid为群id,uid为用户id
hincrby msg:noreadcount:gid uid 1  
#读取
HGET msg:noreadcount:gid uid

3.IM架构图解

基础架构

升级架构

4.展望未来

随着业务的不断发展,IM架构可能需要不断优化和扩展,以满足更高的性能和更多的业务需求。以下是一些可能的方向:

  1. 消息可靠性优化: 进一步优化消息可靠投递机制,考虑引入更先进的消息中间件,例如Kafka或者RabbitMQ,以满足未来更大规模和高可靠性的需求。
  2. 在线状态管理: 引入更智能的在线状态管理机制,结合用户活跃度、终端类型等因素,更精准地判断用户的在线状态,提高在线状态的准确性。
  3. 用户体验优化: 改进离线消息服务,确保高性能的同时,提供更灵活的消息拉取策略,以适应用户不同的使用场景和需求。
  4. 数据存储优化: 针对海量历史聊天消息,持续优化存储方案,考虑分库分表、数据分区等手段,提高历史消息的检索效率。
  5. 系统监控和日志: 强化系统监控和日志记录,及时发现潜在问题,提高系统的稳定性和可维护性。
  6. 前沿技术应用: 关注并尝试引入前沿的通信技术、人工智能等,以提供更智能、更便捷的沟通体验。

公司IM架构的设计和优化是一个复杂而持续的过程,需要不断地根据业务需求和技术发展进行调整。通过深入思考和不断的实践,可以更好地理解架构的本质,为企业通信提供更强大、可靠的支持。

Netty源码解析

www.processon.com/embed/63341...

相关推荐
马尚道1 天前
【韩顺平】尚硅谷Netty视频教程
netty
马尚道1 天前
Netty核心技术及源码剖析
源码·netty
moxiaoran57531 天前
java接收小程序发送的protobuf消息
websocket·netty·protobuf
马尚来2 天前
尚硅谷 Netty核心技术及源码剖析 Netty模型 详细版
源码·netty
马尚来2 天前
Netty核心技术及源码剖析
后端·netty
失散138 天前
分布式专题——35 Netty的使用和常用组件辨析
java·分布式·架构·netty
hanxiaozhang201810 天前
Netty面试重点-1
网络·网络协议·面试·netty
mumu1307梦20 天前
SpringAI 实战:解决 Netty 超时问题,优化 OpenAiApi 配置
java·spring boot·netty·超时·timeout·openapi·springai
9527出列1 个月前
Netty源码分析--Reactor线程模型解析(二)
netty
若水不如远方1 个月前
Netty的四种零拷贝机制:深入原理与实战指南
java·netty