消息系统设计与实现

文/JC_Huang(简书做者)
原文连接:http://www.jianshu.com/p/f4d7827821f1
著做权归做者全部,转载请联系做者得到受权,并标注“简书做者”。

产品分析

首先咱们来看一下市场上关于消息的实现是怎么样的。javascript

简书

简书的消息系统主要分了两种java

  • 简信
  • 提醒

简信
简信的性质其实跟私信是同样的,是用户发送给用户的一则消息,有具体的信息内容。ios


简书简信

提醒
而提醒,则是系统发送的一则消息,其文案格式是固定的,而且对特殊对象通常拥有超连接。json


简书提醒

知乎

知乎跟简书同样,主要分了两种:数组

  • 私信
  • 消息

私信
跟简书同样,使用户发送给用户的一则消息,也能够是管理员发送给用户的消息。post


知乎私信

消息
知乎的消息比简书的提醒有过之而无不及,知乎会对多条类似的消息进行聚会,以达到减轻用户阅读压力的体验。单元测试


知乎消息

消息的三种分类

经过两种产品的简单分析,得出他们的消息有两种分类,在这基础上,咱们再加上一种:公告。
公告的主要性质是系统发送一则含有具体内容的消息,站内全部用户都能读取到这条消息。
因此,消息有三种分类:测试

  1. 公告 Announce
  2. 提醒 Remind
  3. 私信 Message

提醒的语言分析

咱们从简书取一组提醒样本:网站

  • 3dbe1bd90774 关注了你
  • magicdawn 喜欢了你的文章 《单点登陆的三种实现方式》
  • 无良程序 喜欢了你的文章 《基于RESTful API 怎么设计用户权限控制?》
  • alexcc4 喜欢了你的文章 《在Nodejs中贯彻单元测试》
  • 你在《基于RESTful API 怎么设计用户权限控制?》中收到一条 cnlinjie 的评论
  • 你的文章《Session原理》已被加入专题 《ios开发》

分析句子结构,提醒的内容无非就是ui

「谁对同样属于谁的事物作了什么操做」
「someone do something in someone's something」

someone = 提醒的触发者,或者发送者,标记为sender
do something = 提醒的动做,评论、喜欢、关注都属于一个动做,标记为action
something = 提醒的动做做用对象,这就具体到是哪一篇文章,标记为target
someone's = 提醒的动做做用对象的全部者,标记为targetOwner

这就清楚了,sender和targetOwner就是网站的用户,而target是具体到哪一篇文章,若是提醒的对象不只仅局限于文章,还有其余的话,就须要增长一项targetType,来标记目标是文章仍是其余的什么。而action,则是固定的,整个网站会触发提醒的动做可能就只有那几样:评论、喜欢、关注.....(或者其余业务须要提醒的动做)

消息的两种获取方式

  • 推 Push
  • 拉 Pull

以知乎为例
推的比较常见,须要针对某一个问题维护着一张关注者的列表,每当触发这个问题推送的条件时(例若有人回答问题),就把这个通知发送给每一个关注者。

拉的相对麻烦一点,就是推的反向,例如每一个用户都有一张关注问题的列表,每当用户上线的时候,对每一个问题进行轮询,当问题的事件列表出现了比我本来时间戳大的信息就进行拉取。

而咱们则根据消息的不一样分类采用不一样的获取方式
通告和提醒,适合使用拉取的方式,消息产生以后,会存在消息表中,用户在某一特定的时间根据本身关注问题的表进行消息的拉取,而后添加到本身的消息队列中,

信息,适合使用推的方式,在发送者创建一条信息以后,同时指定接收者,把消息添加到接收者的消息队列中。

订阅

根据提醒使用拉取的方式,须要维护一个关注某一事物的列表。
这种行为,咱们称之为:「订阅」Subscribe

一则订阅有如下三个核心属性

  • 订阅的目标 target
  • 订阅的目标类型 targetType
  • 订阅的动做 action

好比我发布了一篇文章,那么我会订阅文章《XXX》的评论动做,因此文章《XXX》每被人评论了,就须要发送一则提醒告知我。

订阅的规则还能够扩展
我喜欢了一篇文章,和我发布了一篇文章,订阅的动做可能不同。
喜欢了一篇文章,我但愿我订阅这篇文章更新、评论的动做。
而发布了一篇文章,我但愿我只是订阅这篇文章的评论动做。

这时候就须要多一个参数:subscribReason
不一样的subscribReason,对应着一个动做数组,
subscribReason = 喜欢,对应着 actions = [更新,评论]
subscribReason = 发布,对应着 actions = [评论]

订阅的规则还还能够扩展
用户可能会有一个本身的订阅设置,好比对于全部的喜欢的动做,我都不但愿接收。
好比Knewone的提醒设置


Knewone提醒设置

因此咱们须要再维护一个表:SubscriptionConfig,来存放用户的提醒设置。
而且,当用户没有提醒设置的时候,可使用系统提供的一套默认设置:defaultSubscriptionConfig

聚合

若是我发布了一篇文章《XXX》,在我不在线的时候,被评论了10遍,当我一上线的时候,应该是收到十条信息相似于:「谁谁谁评论了你的文章《XXX》」?
仍是应该收到一条信息:「甲、乙、丙、丁...评论了你的文章《XXX》」?

知乎在聚合上作的很优秀,要知道他们要实现这个仍是挺有技术的:
知乎的消息机制,在技术上如何设计与规划?
网站的消息(通知)系统通常是如何实现的?

关于这部分功能,咱们尚未具体的实现方法,暂时也没法讲得更加详细。⊙﹏⊙

五个实体

经过上面的分析,大概知道作这个消息系统,须要哪些实体类:

  1. 用户消息队列 UserNotify
  2. 用户 User
  3. 订阅 Subscription
  4. 订阅设置 SubscriptionConfig
  5. 消息 Notify
    • 通告 Announce
    • 提醒 Remind
    • 信息 Message

行为分解

说了这么多,整理一下整个消息流程的一些行为:

  • 系统或者管理员,建立消息
    • createNotify (make announce | remind | message)
  • 用户,订阅消息,取消订阅
    • subscribe, cancelSubscription
  • 用户管理订阅设置
    • getSubscriptionConfig, updateSubscriptionConfig
  • 用户,拉取消息
    • pullNotify (pull announce | remind | message | all)
  • 用户,查询消息队列
    • getUserNotify(get announce | remind | message | all)
  • 用户阅读消息
    • read
 
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

模型设计

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
消息表,咱们须要targettargetType字段,来记录该条提醒所关联的对象。而action字段,则记录该条提醒所关联的动做。
好比消息:「小明喜欢了文章」
则:

target = 123, // 文章ID targetType = 'post', // 指明target所属类型是文章 sender = 123456 // 小明ID

Save Announce and Message
固然,Notify还支持存储公告和信息。它们会用到content字段,而不会用到targettargetTypeaction字段。

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的建立,主要经过两个途径:

  1. 遍历订阅(Subscription)表拉取公告(Announce)和提醒(Remind)的时候建立
  2. 新建信息(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, // 喜欢 }

在这套模型中,targetTypeaction是能够根据需求来扩展的,例如咱们还能够增长多几个动做的提醒: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)

  1. 往Notify表中插入一条公告记录

createRemind(target, targetType, action, sender, content)

  1. 往Notify表中插入一条提醒记录

createMessage(content, sender, receiver)

  1. 往Notify表中插入一条信息记录
  2. 往UserNotify表中插入一条记录,并关联新建的Notify

pullAnnounce(user)

  1. 从UserNotify中获取最近的一条公告信息的建立时间: lastTime
  2. lastTime做为过滤条件,查询Notify的公告信息
  3. 新建UserNotify并关联查询出来的公告信息

pullRemind(user)

  1. 查询用户的订阅表,获得用户的一系列订阅记录
  2. 经过每一条的订阅记录的targettargetTypeactioncreatedAt去查询Notify表,获取订阅的Notify记录。(注意订阅时间必须早于提醒建立时间)
  3. 查询用户的配置文件SubscriptionConfig,若是没有则使用默认的配置DefaultSubscriptionConfig
  4. 使用订阅配置,过滤查询出来的Notify
  5. 使用过滤好的Notify做为关联新建UserNotify

subscribe(user, target, targetType, reason)

  1. 经过reason,查询NotifyConfig,获取对应的动做组:actions
  2. 遍历动做组,每个动做新建一则Subscription记录

cancelSubscription(user, target ,targetType)

  1. 删除usertargettargetType对应的一则或多则记录

getSubscriptionConfig(userID)

  1. 查询SubscriptionConfig表,获取用户的订阅配置

updateSubscriptionConfig(userID)

  1. 更新用户的SubscriptionConfig记录

getUserNotify(userID)

  1. 获取用户的消息列表

read(user, notifyIDs)

  1. 更新指定的notify,把isRead属性设置为true

时序图

提醒的订阅、建立、拉取


提醒的订阅、建立、拉取


咱们能够在产品建立以后,调用NotifyService.subscribe方法,
而后在产品被评论以后调用NotifyService.createRemind方法,
再就是用户登陆系统或者其余的某一个时刻调用NotifyService.pullRemind方法,
最后在用户查询消息队列的时候调用NotifyService.getUserNotify方法。

公告的建立、拉取


公告的建立、拉取


在管理员发送了一则公告的时候,调用NotifyService.createAnnounce方法,
而后在用户登陆系统或者其余的某一个时刻调用NotifyService.pullAnnounce方法,
最后在用户查询消息队列的时候调用NotifyService.getUserNotify方法。

信息的建立


信息的建立


信息的建立,只须要直接调用NotifyService.createMessage方法就能够了,在下一次用户查询消息队列的时候,就会查询这条信息。

相关文章
相关标签/搜索