美文网首页笔戈 Web TeamiOS技术专题软件设计
消息系统设计与实现「下篇」

消息系统设计与实现「下篇」

作者: JC_Huang | 来源:发表于2015-11-15 18:16 被阅读10779次

    关联文章:消息系统设计与实现「上篇」

    模型设计

    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方法就可以了,
    在下一次用户查询消息队列的时候,就会查询这条信息。

    如果本文对您有用
    请不要吝啬你们的Follow与Start
    这会大大支持我们继续创作

    「Github」
    MZMonster :@MZMonster
    JC_Huang :@JerryC8080

    相关文章

      网友评论

      • 甘伍觉莫:私信的话 怎么来处理呢?
      • 3ff7a47c791f:楼主defaultSubscriptionConfig 默认值放在哪里好呢?
      • 417f78ce376d:感觉Notify和UserNotify的表设计不合理, 用户和消息是一个一对多的关系, 一条消息永远只能属于某个用户, 所以实际上在 notify 表上加一个用户 id 即可, 而不用再去新增一个中间件, 加重设计
      • Fred_6668:不知道lz还在吗
        有几个小问题
        1:消息内容的结构体拼接你们是在什么时候做的,(比如用户调用一个评论 ,那你的消息肯定需要包含(评论人id,评论内容id,评论内容标题,内容作者信息等)这块消息结构的数据获取拼接是在评论接口调用成功之后还是说在什么地方)
        2:我发布了一篇文章,那肯定需要订阅对这篇文章的点赞,评论,喜欢等一系列的操作,这些是怎么确定的,是在业务代码中事先写死的吗
        3:因为你的数据库设计里面。targetType和action都是字符串的形式,那是不是表示在代码中也是需要把这块事先写死或者说是写在配置文件里,以及客户端也需要知道这些,如果后续有新增的行为,那以前的已经有的订阅事件怎么更新?

        我们现在也需要设计一套消息系统,作者的文章给了我很大的启发,但是还是有些问题没理明白
        谢谢!
      • 蓝瘦香菇面丨iXtra:题主是做前端的吗
      • 蓝瘦香菇面丨iXtra:提醒记录有问题,万一没人去订阅,那还傻乎乎的去插入提醒表?用户多了这不在浪费数据库吗??????
      • 冰下男:写的很赞
      • d1f537543954:pullRemind的逻辑是不是有问题,按照文中描述的逻辑感觉会重复创建UserNotify
        蓝瘦香菇面丨iXtra:是的,其实大多数人都没有仔细看这篇文章
      • d5295da3df36:博主太给力了,简明扼要的说明了消息系统设计,如果能提供demo就更加完美了
      • ff6ef1d66af4:对于评论和回复提醒怎么弄?

        如果我不使用订阅的方式:

        是不是要在notify中加入上一篇文章中所提到的 `target_owner`字段。

        如果是评论提醒,那么target就是文章id,content就是评论的内容,target_owner是文章的拥有者。

        如果是回复提醒,那么target就是评论id,content就是回复的内容,target_owner是评论的拥有者。
        结果就是谁在什么时候对谁的什么干了什么事情,是这样吗?


        如果使用订阅的话:

        那么比如评论提醒就是用户在创建一篇文章的时候就在订阅表中加入订阅,订阅的就是这篇文章,动作就是评论,喜欢等。

        如果是回复提醒,用户没回复一个评论就是订阅表中加入订阅,订阅的内容就是用户发出的评论。

        是这样吗?
        2b5371e20a31:@tmsdhz “订阅表会爆炸”,这个不了解你现在的业务。就拿文章里面提到的话。一般是1x1. 就是一个行为,产生了一条订阅。个别是1x2
        举例:
        (行为)用户A评论了文章。(订阅)该评论的赞。
        所以这个数据量理论上能大概算出来。

        “不适用订阅那种方式”
        我没看懂你的意思, 没有订阅的话,是如何产生“用户消息UserNotify的”。看看作者的时序图。

        “你有更好的解决方案”
        :blush: 没有在向作者学习
        ff6ef1d66af4:@mT_a018 是的,你说这个对,我有点觉得这个订阅表会爆炸的感觉,从设计上来说,很巧妙。我有点想直接用我说的不适用订阅那种方式来做,你有什么建议吗?或者你有更好的解决方案?
        2b5371e20a31:我的理解,这里不对:
        “如果使用订阅的话:
        如果是回复提醒,用户没回复一个评论就是订阅表中加入订阅,订阅的内容就是用户发出的评论。”

        如果是回复提醒业务就是:如用户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"
      • ff6ef1d66af4:我想问的是

        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” 目标类型比如有产品,文章被某个用户订阅了,那么该字段是直接存成“产品,文章”。还是存单个目标类型好?
        ff6ef1d66af4:@tmsdhz 得到
        ff6ef1d66af4:@tmsdhz 1213
        2b5371e20a31:
        “遍历动作组,每一个动作新建一则Subscription记录”

        一条记录一个目标类型(可重复)
        一条记录一个动作。
      • 海阔天空888:获益匪浅,多谢
      • uimeet:如果我subscription有几千条订阅,那find_subscription这个操作的结果体量也不小了,一起去查notify表么?
      • 小聪明李良才:个人爱好基于这篇文章写了个php的实现 https://github.com/zhuanxuhit/messager,项目是基于php laravel写的,只写了后端的services,没有写前端界面的(ps.因为还没开始学习,等学了就把前端补上去的),然后在原文的基础上对于UserNotify表新增了notify_type的,方便查询,然后原文中说的基于时间的查找,我都改为了基于mysql自增id的,通过id来区分时间先后的
        ff6ef1d66af4:如果我不使用订阅的方式:

        是不是要在notify中加入上一篇文章中所提到的 `target_owner`字段。

        如果是评论提醒,那么target就是文章id,content就是评论的内容,target_owner是文章的拥有者。

        如果是回复提醒,那么target就是评论id,content就是回复的内容,target_owner是评论的拥有者。
        结果就是谁在什么时候对谁的什么干了什么事情,是这样吗?
        JC_Huang:@超级个体颛顼 棒棒哒。
      • f3d0300e5106:两篇文章非常系统的给出了一个漂亮的解决方案。有个问题想请教一下,有推送需求的话,怎么加入到整个运行流程里?定时所有用户pull,然后推送吗?感觉有点不合理。
        f3d0300e5106:@JC_Huang 了解了,谢谢~
        JC_Huang:@圈泉 推送需求,在这个系统中属于「公告 Announce」,在新建一则公告的时候,直接往「UserNotify」表中插入数据。`pullAnnounce`函数,就是查询这个表。我们的做法是,前端用户在刷新页面的时候,请求`pullAnnounce`函数,就能拿到公告的消息。
        如果,你想做到的是那种实时通知的,可以加入`socket`,让服务端和客户端保持长连接,利用事件监听方式通讯。
      • ouyangan:确实写的很好 , 周末根据楼主的文章好好实践下
      • zhenkuo:写的非常清晰,看了几个消息相关实现都不理想,你的文章看了之后很受启发~
        JC_Huang:@zhenkuo 感谢,还有,不客气😁
      • e954c2d01172:还有个问题啊,我关注了一个用户,我要订阅这个用户的所有动作,但是这个订阅表是针对一个具体物品的id,比如我订阅用户的帖子回复动作
        target: 123, // 帖子的ID
        targetType: '帖子',
        action: '回复',
        user: 123 // 我的ID
        这里就需要帖子的ID但是不可能遍历所有的帖子
        2b5371e20a31:“订阅这个用户的所有动作” 这个本事可以在细化。
        例如:订阅这个用户(uid=122)的发布帖子动作。
        target : 122, // 这个用户UID
        targetType : '用户', // 目标的类型
        action : '发布帖子', // 订阅动作
        user : 123 //我的uid

        然后在用户发布帖子时:记录一条Notify
        f3d0300e5106:@wuzhaozhongguo 我觉得你这个需求其实是timeline,已经不是这篇文章讲的消息系统了。需求不一样了,架构必然会有所区别 :relaxed:
      • e954c2d01172:我推送给客户端的标题要在消息服务中拼接吗,比如xxxx评论了你 :这里是评论的内容
        JC_Huang:@wuzhaozhongguo 我们的实现是在前端拼接,后端只输出一个结构。这方便了后面需求的改动
      • 585768e2bd9e:这个架构是不是将各类消息都分发到具体的某个Uid?
        如果是公告,分发到百万用户,那需要创建百万笔数据。
        感觉有点麻烦,有其他方案吗?
        anonymous_e2bc:反一下
        2b5371e20a31:因为百万用户都需要标记, 某条公告是否已读。
      • 25a09dc5ae89:很不错
      • needrunning:请问 NotifyConfig 是以配置文件还是数据库的形式存在,这块不是很理解
        JC_Huang:@needrunning 我们是以配置文件写到内存里面的,当然如果有动态修改配置的需求的话,就需要存到数据库了。
      • 178bff219b76:可否提供用例以供学习?
        小聪明李良才:个人爱好基于这篇文章写了个php的实现 https://github.com/zhuanxuhit/messager,欢迎讨论的

      本文标题:消息系统设计与实现「下篇」

      本文链接:https://www.haomeiwen.com/subject/iwekhttx.html