意义
IM发展至今,已是非常重要的互联网应用形态之一,尤其移动互联网时代,它正以无与论比的优势降低了沟通成本和交流门槛,对各种应用形态产生了深远影响
需求背景
新业务线开发新app需要私信功能,期望实现一套通用私信系统支持创新业务的快速迭代,总结有以下需求:
1、支持文字,语音等多种聊天格式可扩展
2、消息及时通知与及时到达
3、可以多端同步历史消息
4、消息展示严格遵守时间序
5、支持多类型联系人
6、支持消息撤回、删除
7、*支持多业务线扩展
名词解释
新消息:当用户在聊天页面内时,推送到达,用户拉取的消息叫做新消息;
历史消息:当用户不在聊天页面内时,推送到达,用户未拉消息,用户再次进入聊天页面时拉取的消息叫做历史消息;
多端同步:当一个账号更换手机或者换包后客户端数据和之前幂等;
增量更新:在队列队尾添加,通过redis zset的特性,每次都取(old msg_id,inf+)区间的数据完成增量更新;
msg_id:标示唯一一条消息。
方案选择
常见具有IM功能软件,如微信、QQ、钉钉、探探等;
不支持 多端同步, 只支持电脑端同步最近消息 | 多终端同步, 只同步最近消息 | 变态级 多终端同步 | 多终端同步 |
微信 | QQ | 钉钉 | 探探 |
首先支持多端同步,那么竞品选型为QQ,钉钉和探探,历史消息可以回溯,那么只有钉钉和探探符合,又因为在调研过程中钉钉会有一步把所有历史消息全部拉下来的变态级操作(此操作会导致程序初次启动卡住几分钟),最终选择探探这种IM对标。
IM逻辑&&IM结构逻辑
![](https://i-blog.csdnimg.cn/blog_migrate/21321f88527dda61fd039ee93450913e.jpeg)
message.fe属于逻辑业务层,包含各种产品规则、安全校验等非私信核心逻辑,IM和业务相关的操作会在这里进行逻辑,聚合和组装,同时投递发信事件,提供给大数据或者其他业务方使用单向依赖message.base,长链接服务和其他服务。
message.base 层为私信核心逻辑,不包含任何产品策略或者规则限制,不依赖任何三方服务。私信的发送和列表获取采用推拉结合方式,新消息推送给客户端,老消息通过列表拉取,如图:
message.base 发送&接受信息过程时序
时序图
![](https://i-blog.csdnimg.cn/blog_migrate/d530fa87ca104262f7396fc7b3dd4cde.jpeg)
具体细节
新消息同步:如图,客户端更新属于增量更新,依赖更新队列,更新对列按照版本号verison_id进行排序,这里不用msg_id的原因为执行撤回、删除等操作时,消息唯一,msgid不变,version_id进行自增,根据version_id进行增量同步
![](https://i-blog.csdnimg.cn/blog_migrate/7b734f877fcdc803adbb99fbd8791acc.png)
历史消息:对于客户端来时,查找历史消息的过程如下图。如果本地没有历史消息时,请求会通过本地最后一个msg_id服务端2级缓存(如果设置2级缓存的话)和DB中直接进行查找。
![](https://i-blog.csdnimg.cn/blog_migrate/79f6207de8c7082f1ffd91f191dd5c2a.png)
设计遇到的问题
客户端是通过消息区间同步方法pull消息的,如图,但是又一个问题,就是当同步过一次数据后,再次同步很可能出现消息重叠的问题,设想一下场景:a给b发私信,id从1到5,此时b没有再回复,a接着发送了msg_id为6~15的消息,此时b回来看消息,第一屏是8~15,一次同步8条,再次同步历史消息,则为7~1,为题出现在客户端只需要6,7两条数据而已,1~5就重复了,所以为了简化服务设计,决定舍弃去重操作交给客户端处理去重。
![](https://i-blog.csdnimg.cn/blog_migrate/455d0514b674ef20ce81a163569fc389.png)
解决方案:
![](https://i-blog.csdnimg.cn/blog_migrate/bd5f31865b017d6cbf6eab93955ae5a0.png)
消息推送
怎样保证能够及时同步消息?保证新消息推送到达?
ios客户端 苹果apns推送+长连接
安卓客户端 各家手机系统厂商推送+极光推送+长连接
这里不做长链接的具体解释
mysql设计
MYSQL设计采用分库分表的方式
1、联系人表
私信联系人表是 当用户A和用户B在数据库中不存在私信联系人记录时,用户A给用户B发送的一条私信所产生的记录,用户A给用户B发生的一条私信会产生两条私信联系人信息记录,分别为用户A、用户B各一条,当存在时,则需要更新用户A和用户B所在的两条记录,私信联系人列表即为外层消息列表
按照 owner_id 分表,按照业务需求分多少张表,前期不分库,这样可以将一个用户所有的私信会话全部存储在一张表中
CREATE TABLE `contact_0` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增id,主键',
`owner_id` bigint(20) NOT NULL COMMENT '联系人私信拥有者',
`peer_id` bigint(20) NOT NULL COMMENT '联系人私信对话者',
`last_msgid` bigint(20) NOT NULL COMMENT '最新一次发送时的私信id',
`last_del_msgid` bigint(20) NOT NULL DEFAULT '0' COMMENT '最后一次删除联系人时的私信id,默认值为0',
`version_id` bigint(20) NOT NUL COMMENT '联系人更新版本id',
`create_time` datetime NOT NULL,
`update_time` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `owner_id_peer_id_key` (`owner_id`,`peer_id`),
KEY `owner_id_key` (`owner_id`),
KEY `update_time_key` (`update_time`),
KEY `version_id_key` (`version_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
2、用户私信信息列表
私信信息表是用户A给用户B发送一条私信后产生的一条记录,即用户私信信息列表
CREATE TABLE `info_0` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增id,主键',
`union_id` varbinary(128) NOT NULL COMMENT '私信发送者和接受者组合id,组合规则uid小的在前大的在后用冒号连接',
`send_id` bigint(20) NOT NULL COMMENT '私信发送者id',
`msgid` bigint(20) NOT NULL COMMENT '发送时的私信id',
`status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '私信状态,status=0代表正常;status=1代表撤销;默认值为0',
`seq_id` bigint(20) NOT NULL COMMENT '客户端本地私信序列id',
`version_id` bigint(20) NOT NUL COMMENT '私信状态更新版本id',
`create_time` datetime NOT NULL,
`update_time` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `union_id_msgid_key` (`union_id`,`msgid`),
KEY `msgid_key` (`msgid`),
KEY `union_id_key` (`union_id`),
KEY `status_key` (`status`),
KEY `update_time_key` (`update_time`),
KEY `version_id_key` (`version_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
3、消息内容表
私信内容表是用户A给用户B发送私信的具体内容
CREATE TABLE `content_0` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增id,主键',
`msgid` bigint(20) NOT NULL COMMENT '发送时的私信id',
`status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '私信审核状态,status=0代表正常; status=1代表被审核; 默认值为0',
`type` tinyint(4) NOT NULL COMMENT '私信类型',
`content` varbinary(2048) NOT NULL COMMENT '私信内容',
`create_time` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `msgid_key` (`msgid`),
KEY `type_key` (`type`),
KEY `status_key` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
缓存设计
key 规则:拼接key时owner_id与peer_id小的在前,大的在后
key | 结构 | 成员 | 说明 |
---|
MESSAGE_INFO-%d-%d | hash | filed=msgid value=messageInfo | 私信信息L1key |
MESSAGE_INFO_SECONDARY-%d-%d | hash | filed=msgid value=messageInfo | 私信信息L2key |
MESSAGE_INFO_SIMPLE_LIST-%d-%d | zset | member=msgid score=versionId | 私信msgid列表key 维护固定20个队列成员长度 |
MESSAGE_INFO_HISTORY_LIST-%d-%d | zset | member=msgid score=msgid | 私信历史消息msgid列表key 护固定20个队列成员长度 |
![](https://i-blog.csdnimg.cn/blog_migrate/26bb11939d6199f7b2050714f3a930b9.jpeg)
message_base.MessageBaseService.Send 发送信息
请求示例
curl -d
'{
"send_id":959161, //发送者id,
"receive_id":948008, //接收者id,
"type":12, //消息类型,类型的增加代表content内部格式种类的增加
"content":"{}", //消息信息,json字符串的形式展现
"seq_id":1551619596, //消息序列,一般为时间戳
"without_sender":0 //是否有发送者,0为没有,1为有
}'
'http://127.0.0.1:xxxx/message_base/MessageBaseService/Send' |
返回示例
{
"msgid":1551619596000, //唯一消息标识
"update_time":1551619596,
"all_unread_count":1, //更新了对方未读状态条数
"seq_id":1551619596, //序列id
"info_version_id":15516195961234, //消息版本号
"contact_version_id":15516195961234, //联系人版本号
"dm_error": 0,
"error_msg": 操作成功
} |
message_base.MessageBaseService.ContactList 获联系人列表
请求示例
curl -d
'{
"owner_id":959161, //请求方id,
"peer_id":948008, //对方id,
"msgid":1551619596000, //msgid
"versionId":15516195960000, //消息版本号
}'
'http://127.0.0.1:xxxx/message_base/MessageBaseService/ContactList' |
返回示例
{
"contacts":[
{
"uid":959161, //联系人uid
"last_msg":1551619596000, //最后消息id
"last_del_msgid":1551619596000, //最后删除消息id
"unread_count":0, //消息未读数
"update_time":1551619596,
"version_id": 1551619596000, //最新版本号
"del_status":0,
"sort_id":1551619596,
},
...
]
"next_update_time":1551619596,
"next_version_id":1551619597000,
"dm_error": 0,
"error_msg": 操作成功
} |
message_base.MessageBaseService.FirstScreenList 获取首屏消息
请求示例
curl -d
'{
"owner_id":959161, //请求方id,
"peer_id":948008, //对方id,
}'
'http://127.0.0.1:xxxx/message_base/MessageBaseService/FirstScreenList' |
返回示例
{
"msgs":[
{
"send_id":959161,
"receive_id":948008,
"msgid":1551619596000,
"msg_status":0,
"review_status":0,
"msg_type": 12,
"content":"{}",
"create_time":1551619596,
"update_time:1551619596,
"seq_id":1551619596;
"version_id":1551619596000;
},
...
]
"dm_error": 0,
"error_msg": 操作成功
} |
message_base.MessageBaseService.HistoryList 获取历史消息
请求示例
curl -d
'{
"owner_id":959161, //请求方id,
"peer_id":948008, //对方id,
"msgid":1551619596000, //msgid
"versionId":15516195960000, //消息版本号
}'
'http://127.0.0.1:xxxx/message_base/MessageBaseService/HistoryList' |
返回示例
{
"msgs":[
{
"send_id":959161,
"receive_id":948008,
"msgid":1551619596000,
"msg_status":0,
"review_status":0,
"msg_type": 12,
"content":"{}",
"create_time":1551619596,
"update_time:1551619596,
"seq_id":1551619596;
"version_id":1551619596000;
},
...
]
"next_msgid":1551619596,
"dm_error": 0,
"error_msg": 操作成功
} |
message_base.MessageBaseService.NewList 获取新消息列表
请求示例
curl -d
'{
"owner_id":959161, //请求方id,
"peer_id":948008, //对方id,
"msgid":1551619596000, //msgid
"versionId":15516195960000, //消息版本号
}'
'http://127.0.0.1:xxxx/message_base/MessageBaseService/NewList' |
返回示例
{
"msgs":[
{
"send_id":959161,
"receive_id":948008,
"msgid":1551619596000,
"msg_status":0,
"review_status":0,
"msg_type": 12,
"content":"{}",
"create_time":1551619596,
"update_time:1551619596,
"seq_id":1551619596;
"version_id":1551619596000;
},
...
]
"next_msgid":1551619596,
"dm_error": 0,
"error_msg": 操作成功
} |
message_base.MessageBaseService.ContactDel 删除联系人
请求示例
curl -d
'{
"owner_id":959161, //请求方id,
"peer_id":948008, //对方id,
}'
'http://127.0.0.1:xxxx/message_base/MessageBaseService/ContactDel' |
返回示例
{
"dm_error": 0,
"error_msg": 操作成功
} |
message_base.MessageBaseService.Clear 清空私信记录
请求示例
curl -d
'{
"owner_id":959161, //请求方id,
"peer_id":948008, //对方id,
"msgid":1551619596000;
}'
'http://127.0.0.1:xxxx/message_base/MessageBaseService/Clear' |
返回示例
{
"dm_error": 0,
"error_msg": 操作成功
} |