dylanninin/tower-events

系统设计

Closed this issue · 6 comments

抽象模型

通过观察 event 的具体示例,如:

  • 用户 A 创建了一个任务:someone created a todo
  • 用户 B 给任务指派完成者: someone assigned a todo to an assignee
  • 用户 C 回复了某任务:someone replied a comment to a todo
  • 更多示例具体见 events table

不难发现,event 可以抽象为:[someone] [did an action] [on|with an object] [to|against a target] [at a time].

进一步,统一必要的基础概念,一个 event 的主要组成部分如下:

  • actor:即 [someone],产生该事件的主体,可以是任意实体;一般为用户
  • verb:即 [did an action],描述该事件的具体动作,如 created, assigned, replied
  • object:即 [on|with an object],描述该事件的首要对象,比如 someone created a todo 中的 a todo, someone assigned a todo to an assignee 中的 a todo
  • target:即 [to|against a target],描述该事件的目标对象,可根据具体的 verb 进行解释,如 someone assigned a todo to an assignee 中的 an assignee, someone replied a comment to a todo 中的 todo
  • created_at:即 [at a time],描述该事件产生的时间

根据 Tower 的产品体验可知:

  • F001:一个用户,可以加入多个 team
  • F002:『动态』中的 events 属于整个 team,即该 team 下所有成员均可见;新邀请的成员加入后,可以看到所有动态
  • F003:Tower 的客户端(可能有 Web、WeChat 公众号、iOS/Android App)中动态的字面展示规则一致
    • F003-1:event 主要汇聚在『动态』页面
    • F003-2:从『动态』页面点击某一 event 进入到 target 主页面时,该 target 下会展示相关的 event
  • F004: 动态已经产生,则会进入『冻结』状态,属于历史信息,不会随着 actor, object, target 等更新而同步更新
    • F004-1:更新日程标题后,已生成的相关 event 中的日程标题保持不变
    • F004-2:更新用户头像/昵称后,已生成的相关 event 中的头像/昵称保持不变
    • F004-3:删除实体后,已生成的相关 event 保持不变
  • F005:『动态』可以根据一定条件进行筛选
    • F005-1:根据项目、日历、周报筛选(目前支持:全部 | 项目 )
    • F005-2:根据小组成员筛选
    • F005-3:根据 hashtag 进行筛选(需要单独点击 hashtag,因关注点为动态,搜索+筛选部分不再展开 )
  • F006:『动态』页面默认按时间倒序列表展示,并有简单的聚合
    • F006-1:event 首先按照日期分组聚合,比如 『今』、『昨』
    • F006-2:同一时间段内(这里为一天)紧邻的 event 根据 『项目|日历|周报』 进行分组聚合
  • F007:同时编辑一个任务的完成者、完成时间,会分别产生两条 event 动态

结合以上产品需求,对 event 细化后,增加以下属性:

  • title:即该事件的字面标题,一般根据 actor + verb + object + target + created_at 组成,可以灵活展示
  • content: 即该事件的具体内容,一般根据 object + target 组成,可以灵活展示
  • generator:即产生该事件的应用上下文,可选。例如,以上 event 均是用户在某一个 team 下生成的,属于所有 team 成员共有,此时 generator 为该 team
  • provider:即发布该事件的应用上下文,根据 object + target 确定,可选。例如,以上某些 event 是用户在某一个 context 下生成的,比如 todo/comment 等从属于 project 时,此时 providerproject

必须的事件列表

screen shot 2017-06-01 at 7 42 33 pm

可扩展性

Tower 一个 team 下,可以拥有多个 members, projects, calenders, reports,这些概念实体的主要关系如下:

screen shot 2017-06-01 at 6 08 10 pm

根据 Tower 体验,提取一些动态示例,扩展后的事件列表如下:

screen shot 2017-06-01 at 7 42 45 pm

其他思考和补充

特殊事件

观察以上列表,不难发现,有几个特殊的事件,比如:

  • E005,E006:这一类事件在 object 的关键内容发生变化时产生,比如 Todo 的完成时间、完成者。解决此问题,大致有2种办法

    • 新建实体模型,如 Aduit,来跟踪记录各个 model 的变化,如一个 audit;此时 event[object] 取值为 audit。缺点是:1)增加了一个 model 导致复杂度提升,2)object 此时取值为 audit,语义发生变化,引入了不一致
    • event[object] 中增加额外的属性,来记录其自身此次的变化,这种办法符合直觉、简单,同时没有引入其他复杂度,故选用。
      • audited_attribute: 变化的属性名,比如 assignee
      • old_value:变化之前的取值,比如 bob
      • new_value:变化之后的取值,比如 dylan
  • E017:这一类事件属于特殊的 create 动作,增加了 times (频次、次数),表示重复的次数,根据上下文语义化即为『复制』。这里类似,直接在 event[object] 中增加属性

    • audited_attribute: 属性名,比如 times
    • old_value:变化之前的取值,默认为 0
    • new_value:变化之后的取值,复制10次,则为 10
数据存储
  • event:存储所关联实体信息,而非仅仅 id
    • 从 F004 的表现可知,event 为历史数据,不需要同步更新
    • 从产品形态上看,动态为历史回顾,读写比例很高
    • 因此可以反范式,在 event 中冗余所关联实体信息,避免不必要的查询
object 页面中的动态

观察 Tower 时,发现在 object 页面中, 相关的动态会显示一部分。此时,根据 object + verb 进行筛选即可,提升用户体验的操作 icon 可以根据 verb 来确定

性能优化与架构调整

目前 event 设计为单表存储,同时冗余了关联的实体信息,无需额外查询。若出现性能问题以及业务增长带来的瓶颈,可针对具体情况进行优化。
一般来讲,优先从应用层入手,成本最低;紧接着可以调整技术架构、部署架构等,以适应增长需求。

大致顺序如下:

  • 应用层,代码优化。分析具体问题,优化代码,比如避免 N+1 查询,修正有纰漏的代码,业务逻辑的小简化带来显著的性能提升等
  • 应用层,加缓存。动态页面中已经加载的 events 可以利用 Rails Caching 机制缓存起来,避免重复加载/渲染等
  • 应用层,服务拆分。events 自身独立性很强,可以拆分成单独的服务,增强自身的健壮性,同时与其他业务互不干扰
  • 数据库层,做分库。events 单表独立出来,使用专用数据库实例
  • 数据库层,读写分离。主库可读可写,从库同步 events 数据只读;单表数据集大小不变,读写分离 + 多个数据库实例可分散负载
  • 数据库层,做表分区。events 表做 partitioning,单表数据集大小变化;增加数据库实例可分散负载
  • 应用层 load balance。部署多实例,可横向扩展,使用 nginx/haproxy 做负载均衡

若有问题和调整,会即时更新。

fix typo: aduit -> audit