前言
站内信,相信对于绝大多数后端开发者来说,这是一个相对比较常见的功能,结合消息推送是触达用户带动日活的必要手段,区别就是或简单、或复杂。在此之前(2021-08),由于系统的用户量小,从未仔细思考过这个问题,由于本次负责的业务系统可能比以往的用户更多一些,不得以多了解一些,因此,以下内容仅作为个人的一些学习记录、分享。
一些问题(需求),抛砖引玉
- 10w用户或者更多,为什么是10w而不是50w、100w? 只能说有生之年! 100来个用户讨论这个没什么意义。
- 需要支持个人私信(点对点)、群发(点对面),并记录阅读状态(已读、未读)。
- 需要支持按单位、按角色、按职务群发消息。
初步解决问题(实现需求)
10w用户群发消息,显然在发送消息的时候插入10w条数据是不太现实的,现在大厂的用户量动不动上亿,少的也有几百万,肯定不会这么做。10w的用户量,实际在线的不可能达到这个数字,我们只需要实现 在线push(推送),离线pull(拉取)就可以了,说起来似乎挺简单,那么数据表如何设计?
你使用搜索引擎查到的资料,一般会这么设计:
1.消息内容表(t_message_content)
列名 | 类型 | 备注 |
---|---|---|
id | bigint | 主键 |
content | text | 消息内容 |
2.消息表(t_message)
列名 | 类型 | 备注 |
---|---|---|
id | bigint | 主键 |
send_id | bigint | 发送人id |
receive_group_id | bigint | 接收群组id |
content_id | bigint | 消息内容id |
3.用户消息表(t_message_user)
列名 | 类型 | 备注 |
---|---|---|
id | bigint | 主键 |
message_id | bigint | 消息id(群组消息为群组 id,私信为0) |
status | int | 阅读状态(0:未读,1:已读) |
content_id | bigint | 消息内容id(冗余,方便直接查询内容) |
send_id | bigint | 发送人id |
receive_id | bigint | 接受人id(用户查询自己的消息使用此条件即可) |
以上设计解释:
1.如果是私信消息直接插入到t_message_user即可,使用receive_id条件即可查询到自己的消息
2.群发消息插入到t_message即可,用户查询自己的消息sql语句如下:
1 | select *from t_message where receive_group_id in(用户的用户组) and id not in( |
用户登录或者刷新消息列表时,将查询结果插入 到t_message_user即可。
但是显然,以上设计是无法满足以下需求的:
3.需要支持按单位、按角色、按职务群发消息。
初步修改
2.消息表v2(t_message)
列名 | 类型 | 备注 |
---|---|---|
id | bigint | 主键 |
send_id | bigint | 发送人id |
receive_group_id | bigint | 接收群组id |
content_id | bigint | 消息内容id |
type | int | 群组类型(0:角色,1:职务,2:单位) |
org_id | bigint | 单位id |
那么查询语句修改成以下这样:
用户查询自己的群组消息sql语句如下:
1 | select *from t_message |
用户登录或者刷新消息列表时,将查询结果插入 到t_message_user即可。
以上设计看似可以满足需求,但查询语句效率太低而且冗长,也不利于扩展,但当时我确实就是这么做的(时间太赶实现功能就行…此处省略数万字……),毕竟数据量小,确实能用,也用了一段时间。
优化,进一步解决问题
其实也好解决,只要了解过消息队列 mq、mqtt之类的中间件、协议,基本上就知道怎么设计更好。我们只要把单位、职务、角色这几个群组条件拼凑成一个订阅字符串就行了。
举例:
- org/1 表示群组消息发送给单位id为1下的所有用户
- role/1 表示群组消息发送给所有拥有角色id为1的用户
- job/1 表示群组消息发送给所有拥有职务id为1的用户
- org/1/role/1 表示群组消息发送给所属单位id为1并且拥有角色id为1的所有用户
消息表v3(t_message)
列名 | 类型 | 备注 |
---|---|---|
id | bigint | 主键 |
send_id | bigint | 发送人id |
receive_group | varchar(100) | 群组字符串 |
content_id | bigint | 消息内容id |
那么新的表结构下,用户查询自己的消息sql语句如下:
1 | select *from t_message where receive_group in(用户的订阅数组) and id not in( |
以上就是最终的实现方式,这样不但简洁、效率更高,并且利于扩展,方便用户订阅各种消息类型。
结语
以上只说了主要的东西,实际开发中,有些东西需要做一些取舍,比如用户2年从未登录过,积累的消息几w条,这个怎么办?因此我们要给消息分等级,重要消息永远不能丢失或者保存10年20年(比如存取款消息);至于一般消息,我们设定一个过期时间即可,用户登录是只拉去最近1个月、2个月的消息。
另外,拉取消息时,需要异步分页拉取。
至此,以上为全部内容。