b站评论热度怎么排的,b站评论是最弱智的吗

首页 > 实用技巧 > 作者:YD1662024-01-28 18:40:00

3.5 架构设计 - reply-job

评论异步处理层,主要有两个职责:

1. 与reply-service协同,为评论基础功能的原子化实现,做架构上的补充。

为什么基础功能的原子化实现需要架构的补充呢?最典型的案例就是缓存的更新。一般采用Cache Aside模式,先读缓存,再读DB;缓存的重建,就是读请求未命中缓存穿透到DB,从DB读取到内容之后反写缓存。这一套流程对外提供了一个原子化的数据读取功能。但由于部分缓存数据项的重建代价较高,比如评论列表(由于列表是分页的,重建时会启用预加载),如果短时间内多个服务节点的大量请求缓存未命中,容易造成DB抖动。解决方案是利用消息队列,实现「单个评论列表,只重建一次缓存」。归纳而言,所谓架构上的补充,即是「用单线程解决分布式无状态服务的共性问题」。另一方面,reply-job还作为数据库binlog的消费者,执行缓存的更新操作。

b站评论热度怎么排的,b站评论是最弱智的吗(5)

2. 与reply-interface协同,为一些长耗时/高吞吐的调用做异步化/削峰处理。

诸如评论发布等操作,基于安全/策略考量,会有非常重的前置调用逻辑。对于用户来说,这个长耗时几乎是不可接受的。同时,时事热点容易造成发评论的瞬间峰值流量。因此,reply-interface在处理完一些必要校验逻辑之后,会通过消息队列送至reply-job异步处理,包括送审、写DB、发通知等。同时,这里也利用了消息队列的「有序」特性,将单个评论区内的发评串行处理,避免了并行处理导致的一些数据错乱风险。那么异步处理后用户体验是如何保证的呢?首先是C端的发评接口会返回展示新评论所需的数据内容,客户端据此展示新评论,完成一次用户交互。若用户重新刷新页面,因为发评的异步处理端到端延迟基本在2s以内,此时所有数据已准备好,不会影响用户体验。

一个有趣的问题是,早年间评论显示楼层号,楼层号实际是计数器,且在一个评论区范围内不能出现重复。因此,这个楼层发号操作必须是在一个评论区范围内串行的(或者用更复杂的锁实现),否则两条同时发布的评论,获取的楼层号就是重复的。而分布式部署 负载均衡的网关,处理发评论请求是无法实现这种串行的,因此需要放到消息队列中处理。

存储设计

数据库设计

结合评论的产品功能要求,评论需要至少两张表:首先是评论表,主键是评论id,关键索引是评论区id;其次是评论区表,主键是评论区id,平台化之后增加一个评论区type字段,与评论区id组成一个”联合主键“。

由于评论内容是大字段,且相对独立、很少修改,因此独立设计第3张表。主键也是评论id。

评论表和评论区表的字段主要包括4种:

1. 关系类,包括发布人、父评论等,这些关系型数据是发布时已经确定的,基本不会修改。

2. 计数类,包括总评论数、根评论数、子评论数等,一般会在有评论发布或者删除时修改。

3. 状态类,包括评论/评论区状态、评论/评论区属性等,评论/评论区状态是一个枚举值,描述的是正常、审核、删除等可见性状态;评论/评论区属性是一个整型的bitmap,可用于描述评论/评论区的一些关键属性,例如UP主点赞等。

4. 其他,包括meta等,可用于存储一些关键的附属信息。

评论回复的树形关系,如下图所示:

b站评论热度怎么排的,b站评论是最弱智的吗(6)

以评论列表的访问为例,我们的查询SQL可能是(已简化):

1. 查询评论区基础信息:SELECT * FROM subject WHERE obj_id=? AND obj_type=?

2. 查询时间序一级评论列表:SELECT id FROM reply_index WHERE obj_id=? AND obj_type=? AND root=0 AND state=0 ORDER BY floor=? LIMIT 0,20

3. 批量查询根评论基础信息:SELECT * FROM reply_index,reply_content WHERE rpid in (?,?,...)

4. 并发查询楼中楼评论列表:SELECT id FROM reply_index WHERE obj_id=? AND obj_type=? AND root=? ORDER BY like_count LIMIT 0,3

5. 批量查询楼中楼评论基础信息:SELECT * FROM reply_index,reply_content WHERE rpid in (?,?,...)

产品形态上,单个页面只有二级列表(更多嵌套层次,对应嵌套多次点击),且评论计数也只有两级。若回复数也要无限套娃,则一条子评论的发布,需要级联更新所有的父评论的回复数,当前的数据库设计不能满足该需求。

再者,产品侧定义是,若一级评论被删除,其回复也等价于全部删除,若直接删除,此时也可能出现写放大。因此结合查询逻辑,可以不对回复做更新操作,但是评论区的计数更新操作,需要多减去该一级评论的回复数。

评论系统对数据库的选型要求,有两个基本且重要的特征:

1. 必须有事务;

2. 必须容量大。

一开始,我们采用的是MySQL分表来满足这两个需求。但随着B站社区破圈起量,原来的MySQL分表架构很快到达存储瓶颈。于是从2020年起,我们逐步迁移到TiDB,从而具备了水平扩容能力。

缓存设计

我们基于数据库设计进行缓存设计,选用redis作为主力缓存。主要有3项缓存:

1. subject,对应于「查询评论区基础信息」,redis string类型,value使用JSON序列化方式存入。

2. reply_index,对应于「查询xxx评论列表」,redis sorted set类型。member是评论id,score对应于ORDER BY的字段,如floor、like_count等。

3. reply_content,对应于「查询xxx评论基础信息」,存储内容包括同一个评论id对应的reply_index表和reply_content表的两部分字段。

reply_index是一个sorted set,为了保证数据完整性,必须要判定key存在才能增量追加。由于存在性判定和增量追加不是原子化的,判定存在后、增量追加前可能出现缓存过期,因此选用redis的EXPIRE命令来执行存在性判定,避免此类极端情况导致的数据缺失。此外,缓存的一致性依赖binlog刷新,主要有几个关键细节:

1. binlog投递到消息队列,分片key选择的是评论区,保证单个评论区和单个评论的更新操作是串行的,消费者顺序执行,保证对同一个member的zadd和zrem操作不会顺序错乱。

2. 数据库更新后,程序主动写缓存和binlog刷缓存,都采用删除缓存而非直接更新的方式,避免并发写操作时,特别是诸如binlog延迟、网络抖动等异常场景下的数据错乱。那大量写操作后读操作缓存命中率低的问题如何解决呢?此时可以利用singleflight进行控制,防止缓存击穿。

可用性设计

写热点与读热点

2020年的腾讯的辣椒酱不香了[1],引发一场评论区的狂欢。由于上文所述各类「评论区维度的串行」,当时评论发布的吞吐较低,面对如此大的流量出现了严重延迟。

b站评论热度怎么排的,b站评论是最弱智的吗(7)

痛定思痛,我们剖析瓶颈并做了如下优化:

1. 评论区评论计数的更新,先做内存合并再更新,可以减少热点场景下的SQL执行条数;评论表的插入,改成批量写入。

2. 非数据库写操作的其他业务逻辑,拆分为前置和后置两部分,从数据写入主线程中剥离,交由其他的线程池并发执行。

改造后,系统的并发处理能力有了极大提升,同时支持配置并行度/聚合粒度,在吞吐方面具备更大的弹性,热点评论区发评论的TPS提升了10倍以上。

b站评论热度怎么排的,b站评论是最弱智的吗(8)

上一页123下一页

栏目热文

文档排行

本站推荐

Copyright © 2018 - 2021 www.yd166.com., All Rights Reserved.