在当今信息互联的时代,即时通讯(IM)架构成为企业通信的核心。以公司使用的Netty和Redis构建的IM架构为例,深入探讨其中涉及的功能模块、优化方案以及架构扩展等方面的思考。
1. 架构概述
1.1技术栈选择
Netty:作为通信框架,因为它高效、灵活,支持跨平台
Redis:作为数据存储,因为它提供快速缓存、数据安全、灵活数据结构。这两者的搭配使系统更快、更灵活、更可靠。
1.2 功能模块
用户:功能使用者。
消息:用户之间产生的消息记录(文本,图片,表情,文件,视频,语音通讯,业务等等)。
会话:用户之间聊天产生的联系。
群:多用户之间聊天产生的联系(群主,成员包括权限)。
终端:客户端(web,ios,Android)。
未读数:用户未读消息数。
用户状态:用户在线、离线等。
成员关系:单向好友,双向好友、关注等。
单聊:一对一。
群聊:多人互动。
客服:客服与游客或用户沟通(业务沟通咨询)。
内部业务:涉及公司内部系统通讯、调用。
2.架构优化与扩展
2.1如何保证聊天系统消息的可靠投递(不丢消息)
- 客户端通过Netty发送消息,每条消息带有唯一ID以应对超时或失败,IM服务端用该ID去重。
- 为确保消息不丢失,可以使用RocketMQ的可靠消息机制,提供持久化、容错和可靠的消息传递。
-
- 对于中间件选型思考(常用的MQ产品包括Kafka、RabbitMQ和RocketMQ)
RabbitMQ:
- 适合小到中等规模的项目,特别是对于简单的消息队列需求,比如任务分发、事件处理等。
- 如果对消息队列没有过多的复杂需求,RabbitMQ可以选择。
RocketMQ:
- 适合大规模分布式系统,对高性能和大流量有需求的场景。
- 特别擅长处理需要精确顺序的业务,比如有严格顺序要求的场景。
- 支持分布式事务消息,适用于一些需要事务一致性的业务。
Kafka:
- Kafka是一个高吞吐、分布式的消息队列系统,具有出色的持久性、可靠性和水平扩展性。
- 适用于日志收集、消息系统、用户活动跟踪等。
如何选择:
- 如果项目小而简单,选择 RabbitMQ 就够了。
- 如果项目规模大,对性能和顺序性有严格要求,或者在阿里云上,可以考虑 RocketMQ。
最终,具体选择要根据项目的规模、性能需求以及团队的熟悉程度来决定。
- 通过客户端的ACK确认接收消息的机制来保证不丢消息
2.2离线消息服务保证IM系统的高性能
离线消息,用户不在线时收到的消息。考虑用户上下线可能频繁,通常会在用户上线时主动请求服务端的离线消息。直接从数据库拉消息可能会给数据库带来巨大压力。这里可以选择使用高性能缓存,Redis来存储这些消息,以抗住高并发的访问压力。
当然,Redis一般采用集群架构,多节点。这里可以把用户在提升几个数量级来思考。就需要考虑离线消息在Redis集群是否存储得了。为了更好控制存储空间,需要用到一些存储策略,比如仅保留最近一周或一个月的数据,并限制存储消息的条数,例如最多存储最近的1000条。既能保留必要的离线消息,又能减轻数据库压力。
在分析,历史消息很少有人会把历史消息全部看完,用户上线展示最近的一些离线消息就行。如果在极端情况需要查看更早的消息,可以从数据库查询。小概率的操作对数据库的负担是可以接受的。
2.3 海量历史聊天消息数据存储方案优化
1.发送消息处理
用户A与用户B发送一条消息,需要在消息表里存储两条消息。设计初衷是两条消息是存储在同一张表,随着数据量增大查询越来越慢。
优化:
- 拆分历史消息表,分为两张表。
- 消息内容表(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脚本保证消息未读数的一致性
在公司业务迭代中出现红点问题居多,需要做到三端同步不易,文本、图片、语音电话、内部业务消息等,无法彻底解决红点同步问题,开始想法很简单操作数据库,但是做不到消息的一致性,应保证消息的原子性。
- 未读消息可以用redis来维护,比如用户A给用户B发送信息,需要维护两个redis key,一个是用户B总的未读数key加1,一个是用户A_用户B的未读数key加1,这两个key的加1操作,需要保证原子性,可以用lua脚本实现。
- 为什么要单独维护一个总的未读数,其实可以对消息会话的未读数求和就可以了,这里考虑到用户的消息会话比较多的话,会出现性能瓶颈,用户每个消息会话的未读数时刻在变化,在求和过程中,可能之前的读取的消息未读数又发生变化了,所以可以在这里单独维护一个总未读数。
- 对于群聊的未读数,可以针对群里每个人维护一个未读数key,比如用hash结构来存储,一个群里的所有用户的未读数可以用一个
shell
#gid为群id,uid为用户id
hincrby msg:noreadcount:gid uid 1
#读取
HGET msg:noreadcount:gid uid
3.IM架构图解
基础架构
升级架构
4.展望未来
随着业务的不断发展,IM架构可能需要不断优化和扩展,以满足更高的性能和更多的业务需求。以下是一些可能的方向:
- 消息可靠性优化: 进一步优化消息可靠投递机制,考虑引入更先进的消息中间件,例如Kafka或者RabbitMQ,以满足未来更大规模和高可靠性的需求。
- 在线状态管理: 引入更智能的在线状态管理机制,结合用户活跃度、终端类型等因素,更精准地判断用户的在线状态,提高在线状态的准确性。
- 用户体验优化: 改进离线消息服务,确保高性能的同时,提供更灵活的消息拉取策略,以适应用户不同的使用场景和需求。
- 数据存储优化: 针对海量历史聊天消息,持续优化存储方案,考虑分库分表、数据分区等手段,提高历史消息的检索效率。
- 系统监控和日志: 强化系统监控和日志记录,及时发现潜在问题,提高系统的稳定性和可维护性。
- 前沿技术应用: 关注并尝试引入前沿的通信技术、人工智能等,以提供更智能、更便捷的沟通体验。
公司IM架构的设计和优化是一个复杂而持续的过程,需要不断地根据业务需求和技术发展进行调整。通过深入思考和不断的实践,可以更好地理解架构的本质,为企业通信提供更强大、可靠的支持。
Netty源码解析