欢迎大家回到网易云音乐评论技术探秘之旅,上一篇我们介绍了云音乐评论的业务场景和基础架构,这一篇将围绕云音乐评论的核心功能:评论发表、评论获取和评论点赞进行剖析探秘。
评论发表
首先我们先看上图,这是我们把评论功能抽象出来最核心的数据库表结构,为了更好地理解后文,我们先解释一下字段含义
先看一下右边评论表【Comment】:
- id:评论记录主键
- resourceId:评论所属的资源
- contents:评论内容
- time:评论时间
- likedCount:评论被点赞次数(这里会有行锁竞争的问题需要处理)
- status:数据状态
- userId:评论用户ID
- version:版本号,用乐观锁控制数据并发操作
接下来我们看评论与资源关联表【CommentResource】,对资源不太理解的同学可以翻阅上篇。
- id:评论与资源关联表的主键,这里没有使用自增ID,而是使用资源类型和资源ID组合,这样在处理业务逻辑时就可以根据主键ID按照规则拆解出资源ID从而可以方便的取到资源信息
- resourceInfo:资源基本信息
- resourceType:资源类型,如歌单、歌曲、动态、电台等
- CommentCount:记录资源评论数量,类似上表的likedCount都是计数字段,同样有行锁竞争的问题需要处理
评论发表流程
介绍完评论数据模型之后,我们开始进入评论发表流程,整体流程图如下:
从上图可以看到从客户端发起评论到后台服务会先经过网关,上一篇我们也提到了网关的重要性,它为后台服务提供了流控、鉴权等全局能力。接下来就进入应用服务,应用服务首先会进行前置的逻辑判断,包含三部分分别是:一是反作弊判断,主要是安全性方面的判断;二是资源合法性判断,比如资源已经不存在了,或者设置不允许评论等;最后是评论鉴权,有可能资源作者拉黑了某些用户,那么这些在黑名单的用户就不能对该资源进行评论了。
经过上述前置校验通过后,可以看到这里进行了异步化的处理,将评论放到异步消息队列成功时即返回客户端评论成功,再由消息消费者异步进行持久化处理。
对于评论来说,其内容合法性是至关重要的,这个大家都懂,因此在进行真正持久化处理之前需要进行反垃圾处理,将不合适的内容优(he)化(xie)一下,对于反垃圾如何实现这个就比较复杂了,一般都是有专门的部门和系统提供中台化平台,常见的做法是构建内容中心、策略引擎、举报惩罚中心、数据中心和引入人工标注,这个都可以单独再写一篇了,这里暂不赘述。接下来进行真正的持久化,添加评论并更新评论计数,到此评论本身的处理就结束了,然后将该事件写到消息队列,通知其他关联业务处理。
接下来重点说一下上述流程在前置校验和后置持久化中间进行异步化处理,主要有以下两个好处:
- 热门资源可能会在短时间产生大量评论,如果不进行异步化对突发流量削峰会导致在短时间内有大量评论更新同一资源的评论计数,即更新CommentResource表的CommentCount字段,会存在行锁竞争问题
- 评论本身从请求发出到数据落地所需要处理的逻辑相对评论读取要复杂很多,通过异步消息队列将流程解耦可以快速响应用户,提升用户体验。
当然引入异步化也增加了系统复杂度,需要正确使用,否则会引起其他问题。那么异步化需要关注的点有哪些呢?
- 消息有序性:从评论这个业务场景来看,对消息有序性没有那么高的要求,消息不需要逻辑有序,或者说即使消息乱序了,用户感知并不明显,而且消息体已包含评论发表时间,可以确保评论显示的时间与用户发表时间一致。
- 消息幂等性:不同消息中间件提供的机制有所不同,有的是保证最多消费一次,这可能会导致消息丢失;有的是最少消费一次,这可能导致重复消费;对于这个场景来说,肯定是要优先保证消息不丢失,即允许消息存在重复,消息重复引来另一个问题就是要实现消息幂等,否则后果可想而知。这里可以在消息异步化之前生成评论主键作为消息体的一部分,通过成熟的数据库唯一性约束保证消息唯一,相对来说是比较容易也是成本较低的实现方式。
- 消息延迟:异步化的另一个共性问题就是消息可能会延迟,在这个场景中我们看到在消息进入队列之前已经返回客户端成功,此时客户端可以将当前用户评论显示在评论列表上,但是其他人刷新列表时不一定能同时看到,需要等到数据持久化之后,正常情况下客户感知不明显,但在消息堆积比较严重的情况下会存在一定时间差。
评论反垃圾
对于所有互联网UCG相关的场景都避免不了反垃圾处理,对于反垃圾处理在实现上有以下要求:
首先要做到数据可恢复,因为有些数据有可能一开始会被系统认为有问题,后续又经过审核把“有问题”的给恢复回来,因此要求数据资产可恢复,不能做物理删除,实际上不仅仅评论数据,从公司数据资产角度来说对于所有的数据都不建议物理删除。另外,如上文所述反垃圾服务一般是独立于业务系统存在的,跨系统协作不可避免会存在请求或响应没收到的情况,这样会导致整个链路不完整,因此需要有类似对账机制,确保请求被接收,响应被处理,即ACK机制,所以这个地方要求反垃圾记录做持久化处理,做到可追溯。
评论点赞
评论点赞功能很好理解,场景比较简单,喜欢可以点赞,点了赞的也可以取消。但它与评论计数有个类似的问题,热门资源的热门评论有可能会有很多人同时点赞从而产生计数行锁竞争,这里我们同样采用异步的方式进行削峰,但这里与评论有个不同点是这里异步化要重点解决的是消息有序性,因为同一个用户如果先点赞后取消,但后台接收到的消息却是先取消后点赞,这样就会导致最终的结果跟实际操作不符,显然这里消息有序性比消息不重复更重要。
那如何保证消息严格有序呢?消息有序既要求消息有序生产,也要求消息被有序消费。
如上图,我们都知道使用kafka中间件,同一个partition可以保证消息有序,因此我们可以按照用户维护,让同一个用户的点赞和取消点赞操作都写到同一个partition,这可以解决生产端消息有序;然后在消费端,我们使用单线程消费同一个partition,这样可以保证消费端有序消费;但是单线程消费有可能会带来性能问题导致消息积压,因此我们需要考虑扩展更多的partition使消息更分散,这样消费的线程也同样得到相应的扩展。
评论读取
前面我们了解到评论读取的并发是非常高的,因此首先我们要考虑该场景下要保护的核心资源:数据库。为了保护数据库同时为了提供满足业务要求的性能,我们进行了多级缓存的设计,具体如下图:
从下往上看我们可以看到在数据库之上有Memcached缓存,这是跟数据库表一一对应的,我们可以理解为DAO层缓存;再往上是数据组装服务,处理评论相关联的数据组装;接着是使用Redis实现的业务层缓存,这里存放的就是经过业务组装的数据;最上层就是应用服务了,这一层我们也使用了本地缓存,它的特点是快,但容量小,因此只能放少量热门的数据,但这部分最热门数据的缓存可以缓解Redis热点问题,避免最热门数据都打到同一个Redis节点,但本地缓存有一个问题,就是缓存难以同时清除,不同于分布式缓存,使用一个命令就可以把缓存清掉,本地缓存在每个服务器上面都存了一份,这个要同时清理还是比较困难的。
另一个需要注意的是冷热数据的处理,因为数据库和内存缓存的容量级别存在较大差异,我们不可能把所有的数据都缓存起来,因此缓存一般存的都是相对较热的数据,冷数据则从数据库直接加载。
接下来我们重点看一下业务层缓存的设计,这里相对比较复杂。上面提到了这里缓存的是业务组装之后的数据,除了评论数据本身,还会读取其他模块比如用户数据,但其他模块的数据更新又不会通知到评论模块,所以怎么更新这一层缓存呢?这里我们采用的策略不是去订阅其他服务的变更来同步更新缓存,而是采用短时间缓存自动失效后重新拉取数据的方式。从下图可以看到短时间缓存过了2秒之后就需要重新调用数据组装服务,这2秒钟对前端体验来说基本无损,但缓存失效之后会造成大量请求去调用组装服务,为了避免对下层服务造成太大压力这里加了一层分布式锁,即某个缓存失效了之后,在大量请求该缓存数据的线程中,只有一个线程可以调用数据组装服务,其他的请求转到长时间缓存(24小时),当数据组装完成后会同时更新短时间缓存和长时间缓存,这样长短缓存结合的设计既保护了核心资源,也不牺牲服务性能,同时可以解决数据组装问题,可以说是一个比较巧妙的设计。
以上就是云音乐评论的核心功能评论发表、评论获取和评论点赞的实现思路,包含了基础的逻辑架构,处理流程,以及关键功能点实现技巧。