您的位置:首页 > 运维架构 > 网站架构

网站评论功能数据库设计和开发

2017-06-23 15:42 288 查看


网站评论功能数据库设计和开发

来自:http://www.kuqin.com/shuoit/20160113/350008.html

需求分析

现阶段评论做的最好的我想应该是
网易新闻
(app)里面的评论模块了,其“盖楼”的方式让人印象深刻,评论已经成为该app的核心功能之一了。市面上大部分app的评论模块设计的还是相对简单的,这是可以理解的,因为评论模块不是这些app的核心功能之一。

在设计评论模块前可以和pd或者boss沟通,我们的评论功能是核心功能之一吗?实际上,90%的app采用简单的评论设计就可以了,也就是采用
一问一答
,类似于如下的设计。



一问一答

这种设计十分简单、直接,也满足了用户评论、回复的基本要求,对于没有大量用户评论或者评论不是核心功能的app来说就够用了。暂且把这种场景称之为
场景A


如果你是新闻类或者咨询类的app,有着大量的用户评论,那么设计“盖楼”的效果还是可取的,这样能帮助用户找到该条评论或者回复的上下文情景。但是根据“盖楼”的显示效果不同,设计上也是有很大的差别的。如果是以
评论为主
的显示方式,类似于下面的显示方式。



评论为主

这里可以把评论分为
评论
回复
,所有的
回复
均挂在
评论
下面,类似于树状结构。把这种场景称之为
场景B


最后就是类似于网易新闻的评论设计了,贴一张截图



同级显示

这种场景下设计最为复杂,因为回复和评论是同等级的,回复还可以引用完整的回复路径,就是可以溯源到最开始的评论上。这种场景我将至称为
场景C

数据库设计

由于我 一直使用
mysql
,我就以
mysql
为例谈一下针对上面三种场景的设计。


场景A

这种场景下一般评论数量较少,评论不为活跃,可以把不区分评论和回复,而统一看成评论。区别在于有的评论是直接评论
主题
(每个评论都挂在某个主题下,如文章、帖子等),而有些评论是
@
其他用户的,为了能cover这两张场景,使用一张表就可以达到效果,评论表如下设计:
表字段字段说明
id主键
topic_id主题ID
topic_type主题type
content评论内容
from_uid评论用户id
to_uid评论目标用户id
为了能复用评论模块,我们引入一个
topic_type
字段来区分主题的类别。

from_uid
表示评论人的id,通过该id我们可以检索到评论人的相关信息。

to_uid
是评论目标人的id,如果没有目标人,则该字段为空。

出于性能的考虑,往往我们会冗余评人的相关信息到评论表中,比如评论人的nick、头像等,目标用户也是如此。这样一来我们就只用查询单表就可以达到显示的效果。

有时,目标用户有多个,那么可以将
to_uid
字段修改为
to_uids
,保存时用分隔符来分割用户id,而目标用户的信息再去查询缓存或者数据库。也可以简单的将多个目标用户的信息一起存成json格式,可以应付简单的展现需求。


场景B

在以评论为主的树形显示的情况下,数据库的设计十分灵活,可以使用单表,添加一个
parent_id
字段来指向父评论。如果数据库本身支持嵌套查询,那么还是比较方便的,SqlServer、Oracle都支持,但是mysql不支持,那就只能通过存储过程来实现。在互联网应用中,能不使用
触发器
`存储过程`的话,尽量不要去使用,因为其对性能有影响。

我们还可以将评论拆分为
评论表
回复表
评论
挂在各种
主题
下面,而
回复
都挂在
评论
下面。

评论表
的设计如下:
表字段字段说明
id主键
topic_id主题ID
topic_type主题type
content评论内容
from_uid评论用户id
回复表
的设计如下:
表字段字段说明
id主键
comment_id评论ID
reply_id回复目标id
reply_type回复类型
content回复内容
from_uid回复用户id
to_uid目标用户id
由于我们拆分了评论和回复,那么评论表就不再需要目标用户字段了,因为评论均是用户对主题的评论,评论表的设计更佳简洁了。

回复表我添加了一个
comment_id
字段来表示该回复挂在的根评论id,这样设计也是出于性能方面的考虑,我们可以直接通过评论id一次性的捞出该评论下的所有回复,然后通过程序来编排回复的显示结构。通过适当的冗余来提高性能也是常用的优化手段之一。这里给出一段我通过来评论id来查找并组织所有回复的代码:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

public List<ReplyDTO> getReplyListByRid(Long rid) {
List<ReplyDO> replyDOList = replyDAO.queryReplyByCid(rid);
if (replyDOList == null || replyDOList.size() == 0) {
return new ArrayList<>();
}

List<ReplyDTO> replyDTOList = new ArrayList<>();
List<ReplyDTO> parentList = new ArrayList<>();
for (ReplyDO replyDO : replyDOList) {
ReplyDTO replyDTO = convertReplyToDTO(replyDO);
if (replyDTO.getReplyType() == ReplyType.COMMENT) {
replyDTOList.add(replyDTO);
parentList.add(replyDTO);
} else {
boolean foundParent = false;
if (replyDTOList.size() > 0) {
for (ReplyDTO parent : parentList) {
if (parent.getId().equals(replyDTO.getReplyId())) {
if (parent.getNext() == null) {
parent.setNext(new ArrayList<ReplyDTO>());
}
parent.getNext().add(replyDTO);
parentList.add(replyDTO);
foundParent = true;
break;
}
}
}
if (!foundParent) {
throw new RuntimeException("sort reply error,should not go here.");
}
}
}
return replyDTOList;
}

 

reply_type
表示回复的类型,因为回复可以是针对评论的回复(comment),也可以是针对回复的回复(reply),
通过这个字段来区分两种情景。

reply_id
表示回复目标的id,如果reply_type是comment的话,那么reply_id=commit_id,如果reply_type是reply的话,这表示这条回复的父回复。

在数据结构的设计上,我在replyDTO中设计了一个
List<ReplyDTO> next
属性,这样在形成了一个树形的结构,类似如下结构。



树形结构

客户端可以直接根据该结构来进行树形结构的显示。


场景c

要达到网易新闻中评论的效果我还没有特别好的建议。这种场景中评论和回复是同级显示的,回复不在显示结构上不用挂在一个评论下面。双表的设计在这里就不太合适了,因为涉及到评论和回复的混排,使用双表则会导致查询的逻辑过于复杂。所以建议还是采用单表的设计,不区分评论和回复会简化应用层的逻辑。我们统一都看成评论,而有些评论是可以引用其他评论的。本人推荐采用闭包表的设计,例如:

comment表设计
表字段字段说明
id主键
topic_id主题ID
topic_type主题type
content评论内容
from_uid评论用户id
parent_children表
表字段字段说明
id主键
parent_id父id
child_id子id
comment表保存所有评论内容,而parent_children表则记录评论表中各个评论的父子关系。

查询时往往会按照时间排序,我们可以直接按id或者创建时间降序排列查询comment表即可。如果用户想查询一条评论的完整引用,则可以通过parent_children来找到对应的路径。向上查找到评论只需要可执行:

select parent_id from parent_children where child_id=${id} and parent_id != ${id}

向下查找所有的子孙评论可执行:

select child_id from parent_children where parent_id = ${id} and parent_id != ${id}

闭包表在查询时非常方便,但是插入的性能稍差,因为除了插入评论表以外,还需要把该条评论所有的父子关系插入到父子关系表中。插入性能会随着评论层级的加深而线性下降。

海量数据优化

如果你的系统每天都会有成千上万条评论,那么单表的设计肯定是不行,优化的方式也有很多。

分库分表。分库分表是最为常用也最有效的优化方式,建议按照主题来分库分表。这样同一个主题下面的评论就会落到同一张表里,避免了跨表查询。

适当的数据冗余。如果你需要显示评论人的相关信息,那么在插入评论时就把这些信息写入评论表中,避免多次查询。实际上,如果是纪录数据,都可以冗余对应的数据信息,因为它们的数据的实时行和一致性要求并不高,用户不会因为评论中的头像没更新而撕了你,哈哈。

附加幂等数据只允许单项操作。如果pd要求你能给评论点赞,那么你可以告诉他只能点赞,不能取消。因为从幂等性的要求来说,每个赞都是一条记录。评论的赞数如果都从点赞表中统计得出,那么性能开销会十分巨大,而且点赞如此轻量级的一个操作一定会加剧点赞表的竞争操作。所以建议直接在评论表中添加一个
like_count
的计数器,该字段只增不减。

热门评论加缓存。类似于网易新闻的热门评论,读取频度非常高,可以专门开接口给客户端,同时该接口做缓存。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: