关联文章:消息系统设计与实现「上篇」
模型设计
Notify
id : {type: 'integer', primaryKey: true}, // 主键
content : {type: 'text'}, // 消息的内容
type : {type: 'integer', required: true, enum: [1, 2, 3]}, // 消息的类型,1: 公告 Announce,2: 提醒 Remind,3:信息 Message
target : {type: 'integer'}, // 目标的ID
targetType : {type: 'string'}, // 目标的类型
action : {type: 'string'}, // 提醒信息的动作类型
sender : {type: 'integer'}, // 发送者的ID
createdAt : {type: 'datetime', required: true}
Save Remind
消息表,我们需要target
、targetType
字段,来记录该条提醒所关联的对象。而action
字段,则记录该条提醒所关联的动作。
比如消息:「小明喜欢了文章」
则:
target = 123, // 文章ID
targetType = 'post', // 指明target所属类型是文章
sender = 123456 // 小明ID
Save Announce and Message
当然,Notify还支持存储公告和信息。它们会用到content
字段,而不会用到target
、targetType
、action
字段。
UserNotify
id : {type: 'integer', primaryKey: true}, // 主键
isRead : {type: 'boolean', required: true},
user : {type: 'integer', required: true}, // 用户消息所属者
notify : {type: 'integer', required: true} // 关联的Notify
createdAt : {type: 'datetime', required: true}
我们用UserNotify来存储用户的消息队列,它关联一则提醒(Notify)的具体内容。
UserNotify的创建,主要通过两个途径:
- 遍历订阅(Subscription)表拉取公告(Announce)和提醒(Remind)的时候创建
- 新建信息(Message)之后,立刻创建。
Subscription
target : {type: 'integer', required: true}, // 目标的ID
targetType : {type: 'string', required: true}, // 目标的类型
action : {type: 'string'}, // 订阅动作,如: comment/like/post/update etc.
user : {type: 'integer'},
createdAt : {type: 'datetime', required: true}
订阅,是从Notify表拉取消息到UserNotify的前提,用户首先订阅了某一个目标的某一个动作,在此之后产生这个目标的这个动作的消息,才会被通知到该用户。
如:「小明关注了产品A的评论」,数据表现为:
target: 123, // 产品A的ID
targetType: 'product',
action: 'comment',
user: 123 // 小明的ID
这样,产品A下产生的每一条评论,都会产生通知给小明了。
SubscriptionConfig
action: {type: 'json', required: true}, // 用户的设置
user: {type: 'integer'}
不同用户可能会有不一样的订阅习惯,在这个表中,用户可以统一针对某种动作进行是否订阅的设置。而默认是使用系统提供的默认配置:
defaultSubscriptionConfig: {
'comment' : true, // 评论
'like' : true, // 喜欢
}
在这套模型中,
targetType
、action
是可以根据需求来扩展的,例如我们还可以增加多几个动作的提醒:hate
被踩、update
被更新....诸如此类。
配置文件 NotifyConfig
// 提醒关联的目标类型
targetType: {
PRODUCT : 'product', // 产品
POST : 'post' // 文章
},
// 提醒关联的动作
action: {
COMMENT : 'comment', // 评论
LIKE : 'like', // 喜欢
},
// 订阅原因对应订阅事件
reasonAction: {
'create_product' : ['comment', 'like']
'like_product' : ['comment'],
'like_post' : ['comment'],
},
// 默认订阅配置
defaultSubscriptionConfig: {
'comment' : true, // 评论
'like' : true, // 喜欢
}
服务层 NotifyService
NotifyService拥有以下方法:
- createAnnounce(content, sender)
- createRemind(target, targetType, action, sender, content)
- createMessage(content, sender, receiver)
- pullAnnounce(user)
- pullRemind(user)
- subscribe(user, target, targetType, reason)
- cancelSubscription(user, target ,targetType)
- getSubscriptionConfig(userID)
- updateSubscriptionConfig(userID)
- getUserNotify(userID)
- read(user, notifyIDs)
各方法的处理逻辑如下:
createAnnounce(content, sender)
- 往Notify表中插入一条公告记录
createRemind(target, targetType, action, sender, content)
- 往Notify表中插入一条提醒记录
createMessage(content, sender, receiver)
- 往Notify表中插入一条信息记录
- 往UserNotify表中插入一条记录,并关联新建的Notify
pullAnnounce(user)
- 从UserNotify中获取最近的一条公告信息的创建时间:
lastTime
- 用
lastTime
作为过滤条件,查询Notify的公告信息 - 新建UserNotify并关联查询出来的公告信息
pullRemind(user)
- 查询用户的订阅表,得到用户的一系列订阅记录
- 通过每一条的订阅记录的
target
、targetType
、action
、createdAt
去查询Notify表,获取订阅的Notify记录。(注意订阅时间必须早于提醒创建时间) - 查询用户的配置文件SubscriptionConfig,如果没有则使用默认的配置DefaultSubscriptionConfig
- 使用订阅配置,过滤查询出来的Notify
- 使用过滤好的Notify作为关联新建UserNotify
subscribe(user, target, targetType, reason)
- 通过reason,查询NotifyConfig,获取对应的动作组:
actions
- 遍历动作组,每一个动作新建一则Subscription记录
cancelSubscription(user, target ,targetType)
- 删除
user
、target
、targetType
对应的一则或多则记录
getSubscriptionConfig(userID)
- 查询SubscriptionConfig表,获取用户的订阅配置
updateSubscriptionConfig(userID)
- 更新用户的SubscriptionConfig记录
getUserNotify(userID)
- 获取用户的消息列表
read(user, notifyIDs)
- 更新指定的notify,把isRead属性设置为true
时序图
提醒的订阅、创建、拉取
提醒的订阅、创建、拉取我们可以在产品创建之后,调用
NotifyService.subscribe
方法,然后在产品被评论之后调用
NotifyService.createRemind
方法,再就是用户登录系统或者其他的某一个时刻调用
NotifyService.pullRemind
方法,最后在用户查询消息队列的时候调用
NotifyService.getUserNotify
方法。
公告的创建、拉取
公告的创建、拉取在管理员发送了一则公告的时候,调用
NotifyService.createAnnounce
方法,然后在用户登录系统或者其他的某一个时刻调用
NotifyService.pullAnnounce
方法,最后在用户查询消息队列的时候调用
NotifyService.getUserNotify
方法。
信息的创建
信息的创建信息的创建,只需要直接调用
NotifyService.createMessage
方法就可以了,在下一次用户查询消息队列的时候,就会查询这条信息。
如果本文对您有用
请不要吝啬你们的Follow与Start
这会大大支持我们继续创作
「Github」
MZMonster :@MZMonster
JC_Huang :@JerryC8080
网友评论
有几个小问题
1:消息内容的结构体拼接你们是在什么时候做的,(比如用户调用一个评论 ,那你的消息肯定需要包含(评论人id,评论内容id,评论内容标题,内容作者信息等)这块消息结构的数据获取拼接是在评论接口调用成功之后还是说在什么地方)
2:我发布了一篇文章,那肯定需要订阅对这篇文章的点赞,评论,喜欢等一系列的操作,这些是怎么确定的,是在业务代码中事先写死的吗
3:因为你的数据库设计里面。targetType和action都是字符串的形式,那是不是表示在代码中也是需要把这块事先写死或者说是写在配置文件里,以及客户端也需要知道这些,如果后续有新增的行为,那以前的已经有的订阅事件怎么更新?
我们现在也需要设计一套消息系统,作者的文章给了我很大的启发,但是还是有些问题没理明白
谢谢!
如果我不使用订阅的方式:
是不是要在notify中加入上一篇文章中所提到的 `target_owner`字段。
如果是评论提醒,那么target就是文章id,content就是评论的内容,target_owner是文章的拥有者。
如果是回复提醒,那么target就是评论id,content就是回复的内容,target_owner是评论的拥有者。
结果就是谁在什么时候对谁的什么干了什么事情,是这样吗?
如果使用订阅的话:
那么比如评论提醒就是用户在创建一篇文章的时候就在订阅表中加入订阅,订阅的就是这篇文章,动作就是评论,喜欢等。
如果是回复提醒,用户没回复一个评论就是订阅表中加入订阅,订阅的内容就是用户发出的评论。
是这样吗?
举例:
(行为)用户A评论了文章。(订阅)该评论的赞。
所以这个数据量理论上能大概算出来。
“不适用订阅那种方式”
我没看懂你的意思, 没有订阅的话,是如何产生“用户消息UserNotify的”。看看作者的时序图。
“你有更好的解决方案”
没有在向作者学习
“如果使用订阅的话:
如果是回复提醒,用户没回复一个评论就是订阅表中加入订阅,订阅的内容就是用户发出的评论。”
如果是回复提醒业务就是:如用户B,回复了用户A的评论C,用户A收到提醒。(用户B的回复为D)那么,
用户A在发布评论的时候,创建2条订阅:
1)target=C评论id,target_type=评论,action="回复",user="用户A"
2)target=C评论id,target_type=评论,action="点赞",user="用户A"
在用户B回复评论的时候,做了两件事情:
1)“触发提醒”,新建一条提醒
content : xxx
type : 提醒
target : C评论ID, // 目标的ID
targetType : 评论, // 目标的类型
action : 回复, // 提醒信息的动作类型
sender : 用户B, // 发送者的ID
2)创建1条订阅:
target=D评论回复id,target_type=回复,action="点赞",user="用户B"
Subscription
target : {type: 'integer', required: true}, // 目标的ID
targetType : {type: 'string', required: true}, // 目标的类型
action : {type: 'string'}, // 订阅动作,如: comment/like/post/update etc.
user : {type: 'integer'},
createdAt : {type: 'datetime', required: true}
“targetType” 目标类型比如有产品,文章被某个用户订阅了,那么该字段是直接存成“产品,文章”。还是存单个目标类型好?
“遍历动作组,每一个动作新建一则Subscription记录”
一条记录一个目标类型(可重复)
一条记录一个动作。
是不是要在notify中加入上一篇文章中所提到的 `target_owner`字段。
如果是评论提醒,那么target就是文章id,content就是评论的内容,target_owner是文章的拥有者。
如果是回复提醒,那么target就是评论id,content就是回复的内容,target_owner是评论的拥有者。
结果就是谁在什么时候对谁的什么干了什么事情,是这样吗?
如果,你想做到的是那种实时通知的,可以加入`socket`,让服务端和客户端保持长连接,利用事件监听方式通讯。
target: 123, // 帖子的ID
targetType: '帖子',
action: '回复',
user: 123 // 我的ID
这里就需要帖子的ID但是不可能遍历所有的帖子
例如:订阅这个用户(uid=122)的发布帖子动作。
target : 122, // 这个用户UID
targetType : '用户', // 目标的类型
action : '发布帖子', // 订阅动作
user : 123 //我的uid
然后在用户发布帖子时:记录一条Notify
如果是公告,分发到百万用户,那需要创建百万笔数据。
感觉有点麻烦,有其他方案吗?