对公司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...

相关推荐
dreamlike_ocean7 小时前
即将到来的Netty4.2版本模型的变化
netty
beiback4 天前
Springboot + netty + rabbitmq + myBatis
spring boot·mysql·rabbitmq·mybatis·netty·java-rabbitmq
山塘小鱼儿12 天前
Netty+HTML5+Canvas 网络画画板实时在线画画
java·前端·网络·netty·html5
学海无涯,行者无疆20 天前
通用接口开放平台设计与实现——(31)API服务线程安全问题确认与修复
接口·netty·开放平台·接口开放平台·通用接口开放平台
马丁的代码日记1 个月前
Netty中用到了哪些设计模式
java·开发语言·设计模式·netty
wang09071 个月前
netty编程之整合es实现存储以及搜索功能
大数据·elasticsearch·搜索引擎·netty
huisheng_qaq1 个月前
【netty系列-09】深入理解和解决tcp的粘包拆包
tcp/ip·网络编程·netty·网络通信·粘包拆包·粘包拆包解决方案
大作业管家1 个月前
netty开发模拟qq斗地主
netty·qq斗地主
huisheng_qaq1 个月前
【netty系列-08】深入Netty组件底层原理和基本实现
java·netty·context·eventloop·channelhandler·netty原理及实现
wang09071 个月前
netty编程之结合springboot一起使用
java·spring boot·netty