消息系统的设计与实现

消息系统的设计与实现

在开发 Web 应用的时候,消息系统是一个绕不过的话题,特别是在社交类的应用中。用户的点赞、评论、关注、回复等等,对于电商类的应用,还需要有公告等消息,而这些都需要把操作详情通知到对应的人,而且还要满足个性化的定制。

 需求梳理

我们首先来看看关于消息这一块具体的业务形态是怎么样的:
1.对于知乎的消息聚合
2.对于简书的消息聚合
通过对这些的分析,我们可以得出对于消息一般是分为三类:公告、提醒、私信,这里着重讲一下提醒类的消息。
- 公告就是针对全部用户的消息推送(比如停机维护、业务升级)
- 提醒是指在特定环境下的通知(比如点赞、评论)
- 私信就类似点对点聊天

再来看看一组具体提醒类消息的样本:

  • 张三三 关注了你
  • 李思思 喜欢了你的文章 《消息系统的设计与实现》
  • 赵武武 收藏了你的文章 《消息系统的设计与实现》
  • 周正正 评论了你的文章 《消息系统的设计与实现》

可以看到消息都是有一个统一的范本:

  • sender: 提醒的触发者(比如上文中的张三三)
  • action: 提醒的动作(评论、喜欢、关注)
  • target: 提醒的作用对象(具体的某一篇文章)
  • user: 提醒动作作用对象的所有者(比如文章的作者)

通过这样的分析,就可以明白其中senderuser是网站的用户,而target是具体的作用对象,比如文章(article)、商品(product)、订单(order),action动作对于每个应用来说都是固定的,比如点赞(like)、收藏(collection)、评论(comment)等等。

 个性化配置

对于消息系统来说,个性化配置是不可或缺的需求。我们来看看知乎上面的配置:

对于这样的个性化配置,我们还需要维护一个用户对于某个对象可能产生的动作的关注。对于文章,他可能存在的通知范畴有:点赞、收藏、评论。作者只有去订阅了该文章的这些动作,才能在特定的事件产生的时候给作者推送提醒。比如我发布了一篇文章,那么我会根据自己的配置(比如配置中打开了只接收评论)订阅该文章的动作(那么只订阅评论动作),所以文章每被人评论了,就需要发送一则提醒告知我。
对于订阅的规则,不拘泥于这一些,可以根据不同的业务与通知对象进行拓展,而且假如用户没有配置的话,我们还要提供默认的配置。通过上面的分析,我们可以抽象出三个实体,其中通知(Notify)包括三类(公告、提醒、私信)

  • 通知(Notify)
  • 订阅(Subscription)
  • 配置(SubscriptionConfig)

 数据结构

  1. Notify
    id          : {type: 'integer'}      // 主键
    content     : {type: 'text'}         // 消息的内容
    type        : {type: 'string'}       // 消息类型(公告announce、提醒remind、私信message)
    target_id   : {type: 'integer'}      // 目标的ID(比如文章ID)
    target_type : {type: 'string'}       // 目标的类型(比如文章article)
    action      : {type: 'string'}       // 动作类型(比如点赞like)
    sender_id   : {type: 'integer'}      // 发送者ID
    sender_type : {type: 'string'}       // 发送者类型(前台用户user,管理员admin)
    is_read     : {type: 'integer'}      // 阅读状态
    user_id     : {type: 'integer'}      // 消息的所属者(比如文章的作者)
    created_at  : {type: 'datetime'}     // 时间

    对于targetsender这两个可能不是很理解,对于提醒类的消息我们需要标记作用对象(target)与触发者(sender),举一个简单的例子: 「张三三喜欢了你的文章《我的家乡》」,那么:

    target_id:  123          // 文章ID
    target_type: 'article',  // 指明target所属类型是文章
    sender_id: 123456        // 张三三
    sender_type: 'user'      // 张三三的用户类型(普通用户or管理员)

    上面sender_type区分的目的就是为了在发送公告类的时候保存发送者。而且对于公告和私信类的消息还会用到content字段,而不会用到targetaction字段

2.Subscription

target_id   : {type: 'integer'}      // 目标的ID(比如文章ID)
target_type : {type: 'string'}       // 目标的类型(比如文章article)
action      : {type: 'string'}       // 动作类型(比如点赞like)
user_id     : {type: 'integer'}      // 订阅用户

订阅表的设定是为了个性化配置铺路,当用户发表一篇文章的时候,我们会把文章可能产生的动作都保存到订阅表中,在此之后这个目标触发该动作产生的消息,都会被通知到该用户。比如用户想要接收一篇文章的评论消息,那么订阅的数据表现为:

target_id   : 123        // 目标的ID(文章ID)
target_type : 'article'  // 目标的类型
action      : 'comment'  // 动作类型
user_id     : 1          // 订阅用户

而且有订阅的情况下,还可以实现特定文章才接收通告的需求,同学们可以自行发挥想象。

3.SubscriptionConfig
对于每个订阅动作来说,用户可能并不想要都通知到自己。比如文章的动作有点赞、评论、收藏,但是用户只想接收评论的消息,那么配置就派上用场了。他的数据结构很简单:

config      : {type: 'json'}         // 具体配置
user_id     : {type: 'integer'}      // 订阅用户

其中config是一个特定的 json对象,根据不同的业务配置有所不同,比如对于文章和用户可能有:

{
    comment    : true,
    like       : false,
    collection : false,
    follow     : true       //关注用户
}

当然了并不是每个用户都会去配置,所以我们要提供一组默认的配置,而且配置还可以根据业务进行拓展:

defaultConfig: {
    comment    : true,
    like       : true,
    collection : true,
    follow     : true       //关注用户
}

 业务逻辑

通过上面的分析整个需求已经很明朗了,再详细说下每个功能点的实现过程中,具体的业务逻辑。

  1. 创建提醒
    创建提醒主要的参数是用户、动作、目标,然后到订阅表中查询该用户是否有订阅该目标的该动作,然后再去配置表中查询是否开启了该项配置,进而就可以进行入库处理了。

2.创建订阅
一般情况下,我们都会在创建特定实体的时候同步创建订阅记录。比如用户写了一篇文章,然后文章有评论、点赞、收藏事件,那么我们就要在新建文章后这个节点入库三条记录:

{
    target_id   :  111,
    target_type : 'article',
    action      : 'comment'
    user_id     : 10
}
{
    target_id   :  111,
    target_type : 'article',
    action      : 'like'
    user_id     : 10
}
{
    target_id   :  111,
    target_type : 'article',
    action      : 'collection'
    user_id     : 10
}

然后在文章收到评论(comment)等的时候,就可以去查询并且判定是否通知用户了。

3.针对特定实体设定配置
我们还会有这样的需求,用户发表完文章后不想接收该文章的所有或者特定提醒,那么就可以在订阅表中把不想要的那个订阅动作删除了即可

4.设定已读
对于提醒和私信类的消息,设定已读只要在 notify 表里面设定 is_read 字段即可,但是对于公告类的消息,并没有存储user_id字段,这样如果直接设置的话所有用户拉取到的公告都会被设定为已读。这里提供一个解决方案就是通过 RedisBitmap 来存储每个用户每条公告的阅读状态。

论坛传图片太难受,具体可以查看:https://mp.weixin.qq.com/s/JVvyFu_K0CdJwWb...

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 5年前 自动加精
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
讨论数量: 24

像知乎上面,要是一篇文章有5万次点赞,文章作者好像不会收到5万次提醒。
要是你关注的作者发布100篇文章,好像也只会受到几次提醒
像这样的怎么设计好了
像这样的:
file

5年前 评论

@xuzili 应该是做了聚合操作而已,当你的未读池里的消息大于多少条的时候,会对同一类消息做聚合

5年前 评论

其实社区的编辑器直接粘贴图片就可以,并不麻烦。

5年前 评论

看标题,以为是要实现一个mq呢。

5年前 评论

貌似那里看到过这个文章,作者搬的很卖力

5年前 评论

@overtrue 公众号那边的格式是webp,社区不支持直接拖拽webp格式的

5年前 评论

@酱油君 第一次发公众号的时候,也听到这句话了

5年前 评论

@fanhaobai 能力还没达到这个水准

5年前 评论

@翁航 现在很多好的文章都是搬来搬去的 也分不清谁再是原著作者了

5年前 评论

掘金上面有两篇差不多的文章,作者可以去联系一下

5年前 评论

很棒, 刚好要做这个

5年前 评论
GDDD

感觉一个多态关联就完事了

5年前 评论

@GDDD 尝试一下把消息系统抽离出来作为一个单独的服务

5年前 评论
hareluya

没太懂,这个和Laravel原生的通知系统有啥区别- -

5年前 评论

@hareluya 原生通知只是一个工具,这个结合了具体的业务

5年前 评论

没太看懂Subscription表,假如新增加一个action,那么对于旧文章来说,Subscription表是没有记录的,这个查询就有问题了吧?
有SubscriptionConfig这个总配置表,统一配置不就可以了?

5年前 评论

@kimcastle 这个表的目的是为了存储用户订阅了哪些具体的通知。比如订单的发货通知:
SubscriptionConfig 表是用来保存用户是否订阅了订单的发货通知(是一个全局的概念,没有订阅的话所有订单发货都不会收到通知),那么这时候假如用户只有某一个特定的订单不想接收到发货通知,那么这个表就可以用到了。

5年前 评论

@kimcastle 这在国外是一个很常见的需求,给用户发送某个订单的确认邮件,里面有个“不想接收该订单的后续通知”(其他订单依旧通知)按钮,这时候就用到了

5年前 评论

如果系统后来增加了一个文章转发的通知,怎么处理Subscription表?要把以前系统中的所有文章在Subscription表中再加一条数据吗?或者这么解决,或者是只存不订阅操作的数据是不是更好?

5年前 评论

@overtrue 有个疑问,laravel自带的消息通知好像只能单人对单人的通知,成熟的消息通知系统是全部自己开发还是在原先laravel自带的上面衍生开发?

4年前 评论

@fivenull 这个理解不对,自带的是工具,我们自己开发的是汽车,用工具来造汽车

4年前 评论

@HeYJ 对,按目前设计只能这样,把已经存在的文章都增加 一条到 Subscription 表中,因为假如想要设定作者对某篇文章的通知开关,那么肯定要保存数据;当然了,这样做会有点重,可以考虑下是否可以使用 Redis 的 Bitmap 来储存数据

4年前 评论

合着讲了这么多就说了表设计和开关配置???这些不是curd都能设计出来的吗?还以为实现一个点对点,点对多,多对多的消息推送系统呢

2年前 评论

@PHP大佬 你说的没错:

  1. 这个文章只是说了表的设计和开关配置
  2. 这些 curd 即可设计出来,没有什么技术含量
  3. 消息推送系统本人水平不足,实现不了
2年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!