系统设计
Closed this issue · 6 comments
dylanninin commented
抽象模型
通过观察 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
- F003-1:
- F004: 动态已经产生,则会进入『冻结』状态,属于历史信息,不会随着
actor
,object
,target
等更新而同步更新- F004-1:更新日程标题后,已生成的相关
event
中的日程标题保持不变 - F004-2:更新用户头像/昵称后,已生成的相关
event
中的头像/昵称保持不变 - F004-3:删除实体后,已生成的相关
event
保持不变
- F004-1:更新日程标题后,已生成的相关
- F005:『动态』可以根据一定条件进行筛选
- F005-1:根据项目、日历、周报筛选(目前支持:全部 | 项目 )
- F005-2:根据小组成员筛选
- F005-3:根据 hashtag 进行筛选(需要单独点击 hashtag,因关注点为动态,搜索+筛选部分不再展开 )
- F006:『动态』页面默认按时间倒序列表展示,并有简单的聚合
- F006-1:
event
首先按照日期分组聚合,比如 『今』、『昨』 - F006-2:同一时间段内(这里为一天)紧邻的
event
根据 『项目|日历|周报』 进行分组聚合
- F006-1:
- 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 时,此时provider
为project
dylanninin commented
dylanninin commented
dylanninin commented
其他思考和补充
特殊事件
观察以上列表,不难发现,有几个特殊的事件,比如:
-
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
- audited_attribute: 变化的属性名,比如
- 新建实体模型,如
-
E017:这一类事件属于特殊的
create
动作,增加了times
(频次、次数),表示重复的次数,根据上下文语义化即为『复制』。这里类似,直接在event[object]
中增加属性- audited_attribute: 属性名,比如
times
- old_value:变化之前的取值,默认为
0
- new_value:变化之后的取值,复制10次,则为
10
- audited_attribute: 属性名,比如
数据存储
event
:存储所关联实体信息,而非仅仅 id- 从 F004 的表现可知,
event
为历史数据,不需要同步更新 - 从产品形态上看,动态为历史回顾,读写比例很高
- 因此可以反范式,在
event
中冗余所关联实体信息,避免不必要的查询
- 从 F004 的表现可知,
object
页面中的动态
观察 Tower 时,发现在 object
页面中, 相关的动态会显示一部分。此时,根据 object
+ verb
进行筛选即可,提升用户体验的操作 icon 可以根据 verb
来确定
dylanninin commented
性能优化与架构调整
目前 event
设计为单表存储,同时冗余了关联的实体信息,无需额外查询。若出现性能问题以及业务增长带来的瓶颈,可针对具体情况进行优化。
一般来讲,优先从应用层入手,成本最低;紧接着可以调整技术架构、部署架构等,以适应增长需求。
大致顺序如下:
- 应用层,代码优化。分析具体问题,优化代码,比如避免 N+1 查询,修正有纰漏的代码,业务逻辑的小简化带来显著的性能提升等
- 应用层,加缓存。动态页面中已经加载的
events
可以利用 Rails Caching 机制缓存起来,避免重复加载/渲染等 - 应用层,服务拆分。
events
自身独立性很强,可以拆分成单独的服务,增强自身的健壮性,同时与其他业务互不干扰 - 数据库层,做分库。
events
单表独立出来,使用专用数据库实例 - 数据库层,读写分离。主库可读可写,从库同步
events
数据只读;单表数据集大小不变,读写分离 + 多个数据库实例可分散负载 - 数据库层,做表分区。
events
表做 partitioning,单表数据集大小变化;增加数据库实例可分散负载 - 应用层 load balance。部署多实例,可横向扩展,使用 nginx/haproxy 做负载均衡
若有问题和调整,会即时更新。
dylanninin commented
dylanninin commented
fix typo: aduit -> audit