V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
hustlzp
V2EX  ›  程序员

如何设计 notification 表?

  •  
  •   hustlzp ·
    hustlzp · 2013-12-25 19:57:01 +08:00 · 4502 次点击
    这是一个创建于 3971 天前的主题,其中的信息可能已经有所发展或是发生改变。
    最近在做一个站点的消息通知模块。

    目前的设计是这样的,有2张表:

    user
    ——————————————
    id
    notification_check - DateTime

    notification
    ——————————————
    id
    user_id
    created - DateTime

    * 每有消息产生就往 notification 表塞
    * 某用户获取属于他的通知的话,就在 notification 表中查找满足 notification.user_id == user.id 且 notification.created >= user.notification_check 的所有条目
    * 完了就更新 user 的 notification_check 为 datetime.datetime.now()

    这样的话,基本的消息通知系统就可以运作起来。关键在于如何设计 notification 表的其他字段?

    如果消息的种类相对单一,比如V2EX的消息貌似只有2种:(1)A在话题B里回复了你(2)A在话题B中提到了你。

    这样的话我就可以这样设计 notification 表:

    notification
    ——————————————
    id
    user_id
    created - DateTime
    type - 消息类别,比如“回复”、“提及”(这里用中文只是做示例)
    trigger_id - 触发此消息的用户 id
    topic_id - 与此消息相关的 topic id
    content - 消息主体,对于 V2EX 来说就是回复内容

    这样的话,在用户进入“提醒系统”时,就可以从 notification 表中取出全部未读的消息,然后根据 notification 表的字段生成消息的 HTML。

    但是,如果消息的种类不仅限于论坛类的,而且涉及到其他类别:比如A关注了B / A赞了B(社交网络类消息),A成功付款 / A购买的商品已发货(商城类消息)。

    问题是:如果这些差别非常大的 notification 同时存在,如何设计 notification 表的结构?

    下面想了几种方法:

    (1)大杂烩:

    在一个 notification 表中增加所有需要的字段,之后在为用户构建消息 HTML 的时候不同 type 的消息使用不同 field 就行。

    (2)分离:

    针对不同类的 notification 设计各自的 notification 表,比如 forum_notification、shop_notification 等等。然后在取某 user 的消息时轮询所有的表,提取出所有的消息,然后根据不同的 type 构建结构不同的消息 HTML。

    (3)标准化:

    notification 的字段固定如下:

    notification
    ——————————————
    id
    user_id
    created - DateTime
    header - 消息头 HTML
    content - 消息主体 HTML
    image - 消息左侧的图片 HTML

    这样的话就可以满足所有消息的需求,因为所有的 notification 的最终 HTML 表现形式都可归结为:image(左侧)+ header(上侧)+ body(下侧)。但是这样的话就要求在插入 notification 表的时候要把 HTML 构造好再插入。

    (4)使用 nosql:

    既然不同 type 的消息需要的字段不一样,那么正好使用 schema-less 的 nosql 来构造 notification 表(比如 MongoDB)。


    不知道意思表达清楚没有...

    第一次遇到这种问题,思路比较混乱。求各位指点一二啦!
    23 条回复    2016-06-29 18:06:27 +08:00
    dagger
        1
    dagger  
       2013-12-25 21:46:39 +08:00   ❤️ 1
    既然想到了schemaless,那也可以直接在RDBMS里存JSON啊,notification里搞个data字段,结构化的消息信息转成JSON放里面就可以了
    hustlzp
        2
    hustlzp  
    OP
       2013-12-25 22:16:18 +08:00
    @dagger 不错!
    usoluyun
        3
    usoluyun  
       2013-12-25 22:56:58 +08:00   ❤️ 1
    rdbms里面放json不是好主意,主要是因为如果json过大(超过4k)对性能影响还是非常明显的。有条件的还是考虑nosql,或者还是把消息主体细化拆分到不同的表里。
    heroicYang
        4
    heroicYang  
       2013-12-25 23:27:59 +08:00   ❤️ 1
    RDBMS 的话还是规规矩矩的范式化设计吧,NoSQL 就揉到一个文档好了。
    zzNucker
        5
    zzNucker  
       2013-12-26 00:36:12 +08:00   ❤️ 1
    我是来给lz点赞的,好久没看到这么诚意的问题了。
    gfreezy
        6
    gfreezy  
       2013-12-26 12:02:49 +08:00
    @dagger RDBMS 塞json,会导致没办法按照字段索引
    hustlzp
        7
    hustlzp  
    OP
       2013-12-26 12:06:28 +08:00
    @zzNucker 3x,问问题之前需要做做功课嘛~
    gfreezy
        8
    gfreezy  
       2013-12-26 12:17:57 +08:00
    我们的提醒的结构用的是 1 结构,实际在使用的时候发现提醒的类型数目有限,基本是可穷举的。这样在实际构造HTML代码的时候比较麻烦,但是逻辑上还是比较简单的。

    desc notification:
    id | sender_id | receiver_id | action | target_id | extra | create_time | delete_time

    sender_id: 提醒的发起人
    receiver_id: 提醒接收人
    action: 什么动作触发的提醒
    两部分组成,第一部分为行为的代号,第二部分为目标的代号
    行为代号:
    评论101, 关注102,新朋友103,赞104,做过105,@别人106, 回答(问题) 107, 提问 108, 官方消息109
    目标代号:
    菜谱1001, 用户1002, 讨论区话题1003,作品1005, 菜单1007, 问题1013
    target_id: 触发提醒的动作的承受者
    extra: 动作承受者的补充信息,默认为''
    create_time: 提醒创建时间
    delete_time: 提醒需要被删除的时间


    delete_time表示提醒即将被删除的时间,当delete_time为NULL时,这是一条未读提醒。当delete_time为一个合法的时间时,这是一条已读提醒。到达delete_time时,此条提醒被移动到notification_r1表中,此后此条提醒对用户不可见。

    用crontab定期根据delete_time来清理提醒。
    dagger
        9
    dagger  
       2013-12-26 14:52:15 +08:00
    @usoluyun @gfreezy
    他的消息表和消息内容的各个部分是一对一关系,不是一对多关系,合并到一起不会有规模上的扩张,如果本来就太大的话,分开来存放也一样
    像这类消息应用看样子就用不着对内容进行查询和索引,所以我才这么推荐的,而且这些消息内容基本上是整体读写的,直接结构化成json没什么不妥,如果有场景只用到了其中某一个元素,比如消息头列表,那单独存放也是自然的事
    hustlzp
        10
    hustlzp  
    OP
       2013-12-26 16:11:17 +08:00
    @gfreezy 设计得不错,非常有参考价值,感谢分享!
    alex321
        11
    alex321  
       2013-12-26 16:16:52 +08:00
    可否改变一下思路,采用事件驱动来处理,甚至触发处理呢。
    如果单纯采用表的方式处理,在性能上或者及时响应性上会遭遇纠结状况。
    hustlzp
        12
    hustlzp  
    OP
       2013-12-26 16:35:16 +08:00
    @alex321

    系统中维护一个公用的消息任务队列?有消息产生就往队列里塞,队列的worker负责处理并分发到各user?是这个意思吗?

    在stackoverflow上看到的一个回复:

    http://stackoverflow.com/questions/1315991/design-pattern-notification-system

    "Create a system queue, each message added to this queue has a list of "consumers" and the content. The main message pump processes each message and sends the message to all consumers.

    Lets say 2 people befriend each other. You add a message to the main system queue that A is friends with B and consumers are both A and B. When your message "pump" (processor) sees this message it adds it to the queue of A and the queue of B. So now user A and user B have a new message that they are friends. In turn each user has a message processor, so when it sees a message called "I am friends with [someone]" it processes this as add a new entry to the "wall" visible to friends of A that "A is friends with B", etc.

    This is overly simplistic but hopefully shows how message queues can be used for this (very similar system is used as the windows UI framework) so there is already an existing example and there are plenty of synchonized message queue patterns you can use."
    hustlzp
        13
    hustlzp  
    OP
       2013-12-26 17:04:44 +08:00
    过段时间会把notification的实践经验分享出来
    alex321
        14
    alex321  
       2013-12-26 17:22:34 +08:00
    @hustlzp 类似。如果全部用一个公共队列的话,并发一多估计也是个问题。可以进一步优化一下,公共队列负责队列中时间的捕获,个人的提醒用额外的方式来处理。
    这一类属于基础技术架构的东西最好能有负责架构的童鞋参与讨论。
    hustlzp
        15
    hustlzp  
    OP
       2013-12-26 17:49:12 +08:00
    @alex321 恩,我在这方面经验太少,得找他们请教下~
    gfreezy
        16
    gfreezy  
       2013-12-26 19:09:46 +08:00
    @dagger 如果整存整取,不用对某个字段进行查询的话,并且 NOSQL 的场景不多,直接在 MySQL 里面塞 JSON 吧,另外单独架一个 NOSQL 运维成本太高了。

    @alex321 notification 用 message queue 来做会不会杀鸡用牛刀了。消息队列这个东西太复杂,还得去维护消息队列这个设施的可靠性,丢消息的情况啥的。对一个小系统来说,相关的逻辑被拆分到了多个不同的系统中,维护会比较复杂吧。
    alex321
        17
    alex321  
       2013-12-26 21:58:26 +08:00
    @gfreezy 不清楚题主的设计负载呢,就按照常规的负载来预期的。30w 规模的用户,提醒是基于事件驱动的。典型的状态是,用户 a 在话题中提醒 b,或者 a 评论了 b 的内容,系统将对 b 提醒;并且,b 关注了 a 的情形下,a 的其他操作事件也将触发系统对 b 的提醒。
    如果题主所涉及到的是直接提醒,也就是上述情形中的前者,可以参考下 discuz! 的提醒,相对容易一些。
    gfreezy
        18
    gfreezy  
       2013-12-26 22:18:18 +08:00
    @alex321 b 关注了 a 的情形下,a 的其他操作事件也将触发系统对 b 的提醒。

    这个应该算关注动态了。虽然也算广义上的提醒,但具体实现时和第一种情形分离应该比较简单。
    hustlzp
        19
    hustlzp  
    OP
       2013-12-26 23:11:44 +08:00
    @gfreezy
    @alex321

    目前是第一种情形,所以无需动用message queue。

    后面要做“动态关注”的话,估计用户多了就得上消息队列。
    dawncold
        20
    dawncold  
       2013-12-27 03:12:48 +08:00
    感觉用一个通用的notification表,不同类型的消息只需要title不一样就可以了,用户看到的就是“xxx赞了你的回复”,"xxx回复了你的主题","您购买的商品已发货"等等,等用户点击具体的消息后会看到具体的内容。

    另外根据你的表设计,有一种情况就是,如果一个用户在看消息的时候,可能消息会有很多条你会做分页,那么用户的notification_checked_at = greatest(notification.created_at on this page)
    bombless
        21
    bombless  
       2013-12-27 04:50:09 +08:00
    楼主这种情况的话类似的问题我感觉非常普遍,很关心大家的讨论结果。如果是我的话也很不喜欢在关系型数据库加json的想法,不过我看过的大概三到四个类似实现都是用类似的手段,尽管用的不是json
    hustlzp
        22
    hustlzp  
    OP
       2013-12-27 10:31:45 +08:00
    @dawncold 是的,赞同你的看法。我目前的设计就是只存储文本。
    Pure88
        23
    Pure88  
       2016-06-29 18:06:27 +08:00
    哈哈,我转互联网第一个项目,就干过跟你同样的傻事。
    当时我的 notification 表 就是采用分离设计的。
    如果是现在,我可能会把不同的 notification 设计成 json 协议数据,存到 notification 表里面,上层取出来解析一下,然后生成动态 html 了。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2498 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 15:38 · PVG 23:38 · LAX 07:38 · JFK 10:38
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.