社区点赞系统设计
点赞系统设计
恰好刷到了bilibili技术团队给出了目前B站里点赞系统的架构设计文章,有很多平时开发中意想不到的设计理念,由此写下这篇方案设计文;
参考bilibil的系统设计,从我的角度重构这篇架构设计(狗头.jpg)
点赞的功能与本质
和朋友圈中的点赞,加好友等一个业务主体关联用户关系的功能类似;
点赞的核心本质上就是维护一份用户与业务id的关联关系,在bilibili中这个关联关系所扩展出来的功能包括但不限于:
- 稿件(评论,视频,文章...)的点赞数量
- 用户的获赞总数
- 用户是否点赞
- 与点赞相同的踩功能
- 用户的点赞视频列表
- ...
所以点赞这个功能的实现其实并不难,可以根据上述需求看出都是一些CRUD
级别的同步操作;
难的是将点赞功能抽象出来,成为一个可以通过简单配置为社区型应用赋能的中台化系统,因此需要作为一个高并发级别的独立系统服务进行设计;
由此点赞系统的架构设计本质是:如何在高读写的环境下,稳定支持点赞功能的高效运行
架构设计
取自原文图片
与传统的C端互联网项目类似,上述系统架构的走向是:
- 采用
CDN
分流机房,通过网络IP判断用户最近的部署机房 - 网关进行业务服务的路由负载均衡,鉴权,请求封装等操作
- 主体业务服务完成业务动作,发起异步落库逻辑
- 消息队列
mq
链路执行异步落库指令,完成各项子级系统的事件触发,比如点赞计数+1,取消点赞等等 - 消费,落库
- 以缓存redis,KV数据库,TIDB数据库的先后优先级分别落库结束链路
部分名词会在下面的分析中一一解释
功能分析
点赞所涉及到的功能在上述中已经提及,简单的比喻,比如bilibili评论社区在接入点赞系统后;
通过简单的api调用的方式为当前评论赋予点赞数、是否点赞、踩的功能,而后评论的推荐置顶算法可以基于点赞系统提供的实时点赞数进行刷新。
整个系统从用户动作上看很简单:
- 用户评论
- 评论被点赞
- 将点赞数反馈给用户
所以重点与难点在于如何支撑高并发下的点赞数据的同步与更新;
数据存储设计
根据bilibili技术团队所指,他们采用的是本地缓存,cache缓存,db持久层的三层访问存储数据的模式;
数据结构大体为:
{
"businessId":"业务id",
"messageId":"消息id",
"userId":"用户",
"value":1
}
核心value根据不同业务存储模型不同,比如对于点赞总数,value是一个整数;对于点赞列表,value是一个集合...
存储的内容很简单(因为点赞功能本身并不具备强业务性),但是越是简单的数据面临的问题难点也就越多,因此三个档次的访问数据是必不可少的;
接下来谈谈各层在系统中的定位。
本地缓存
与所有设置本地缓存的系统最终目的一样:减轻cache(redis)缓存层的访问压力;
在点赞系统中,也是为了应对热点视频或评论的访问压力,所以如何利用好本地缓存的要点在于如何找到当前系统中最需要被缓存的热点数据;
热点的发现在此前bilibili技术团队有提过:https://mp.weixin.qq.com/s/C8CI-1DDiQ4BC_LaMaeDBg 作者:哔哩哔哩技术 https://www.bilibili.com/read/cv21576373/
这里简单的举一个例子:
存在一张二维的哈希表,y轴表示视频/文章/评论等等的分类,x轴表示当前分类下的热点数据凭证的hash值;
当有一个点通过hash计数判断为热点数据时,假设当前点热度值为 (1,1)= 10;
当有一个同样的点通过hash计算也被设置到(1,1),则会比较当前点=10,与再次计算到(1,1)位置的热点数据比较二者的热度;
如果后者大于前者,则替换掉当前位置的热点数据,(1,1)= 第二次计算的热点值 - 10;
这样可以保证真正需要被本地缓存的热点数据是实时有效的,最大化的利用有限的本地内存;
cache缓存
指redis缓存的数据,在应用程序直接访问数据库之间必然要设置的缓冲层;
基于点赞系统的数据存储简单性,采用的都是CacheAside
旁路缓存,也就是我们最常使用redis的方式:
应用程序在查询数据时,首先会尝试从缓存中获取数据,如果缓存命中则直接返回数据;如果缓存未命中,则从数据库中查询数据,并将查询到的数据写入缓存。
所以这一层在设计上来说很难存在再优化点,往往考虑的是如何保证redis层的高可用,容灾,数据一致性的问题;
点赞系统中存在列表形的数据内容,比如 用户的历史点赞列表
这个列表在功能上来说有两个含义:
- 判断当前视频/评论/文章是否被点赞
- 展示当前用户的点赞记录
细心的同学在使用b站的时候可能遇到过,在点赞超过第1000个视频时,再次查询自己点赞的第一个视频会发现自己的点赞按钮灰掉了可以再次点击;
指redis缓存点赞过的视频列表只会缓存999个,这也是为了避免产生大Key问题一定需要设置的限制;
但是这样是否会影响用户实际使用,视频的点赞数的实际数量等等...这就是后面要谈的功能上的优化设计了
db存储
点赞数据的实际落库,存储数据的持久层;
在点赞访问如此之大,用户体量庞大的系统中,即使按时间分表也无法控制数据量的愈渐增大带来的种种问题;
所以使用分布式数据库是点赞系统(指的是独立之外的系统,而非小社区中的点赞功能)必不可少的选择;
在业务扩展和开发上无需考虑分库分表的操作,即使随着用户业务量的增大也可以灵活的通过增加机器数据库,扩大服务数进行相应的扩容;
在访问量不大的系统中存储:
简单的关系型存储的关联模式就足以应对点赞功能衍生的各类功能;
但是在以亿为单位的系统中存储,这样的关联关系虽然有效,但是随着数据量的增大会有一种无力感:优化分表做的再好,查询也很难变得更快;
所以除了使用tidb
记录一个时间范围内的查询数据的访问存储层;
还需要定时的对tidb
的所有数据进行数据归档持久化冷存储的动作,而作为数据仓库的中间件只能是非关系型数据库比方mongdb的文档存储结构,存储以下类型的数据:
{
"userId":"1",
"likeList":["1","2","3"]
....
}
key-value的存储结构,也因此b站使用了自研的泰山
数据库用作数据兜底,当数据库崩溃或者发生异常时也不存在数据的全部丢失问题;
点赞功能
点赞功能很简单,简单到就是一个按钮,一个计数和记录;
但是其中会存在各种各样的问题导致功能空有其形,接下来谈谈用户使用过程中会出现的问题以及解决方案;
系统奔溃时
如同在文章中特别指出的一句话:
点赞数、点赞、列表查询、点赞状态查询等接口,在所有兜底、降级能力都已失效的前提下也不会直接返回错误给用户,而是会以空值或者假特效的方式与用户交互。后续等服务恢复时,再将故障期间的数据写回存储。 作者:哔哩哔哩技术 https://www.bilibili.com/read/cv21576373/ 出处:bilibili
不过这种解决方式并非只适用于崩溃时执行,在大多优先考虑用户交互体验的系统中都可以延用:
实际的业务当成订单交给异步线程,在用户角度上看就是点击了这个按钮就一定成功,剩下的就交给系统的各种重试机制、兜底机制、人工干预;
重复点赞
在cache缓存层中有提到已经点赞过的视频会再次亮起点赞按钮,这时是否会出现重复点赞问题?
答案是绝对否定的
首先在社区背景中,以往看过的某个视频/某条评论的是否被点赞的记录是可以容忍一时的数据不相符的;
因此业务层在接入点赞系统之初就需要对当前用户的操作进行行为判断是否可行,从业务上db查询断绝重复点赞的源头;
查询分页
对一个视频下的所有评论进行点赞,假设上限为999;
如果查询这个点赞列表采用的是游标分页,那么点赞列表的记录就一定需要有一个标识用于分页;
如果查询这个点赞列表适用的是全量返回,那么超过999的数据页的评论点赞记录就会消失;
因此这里极大可能是redis+db数据回溯的方式;
仅记录前999条,从第999条开始的数据查询数据库,数据再以短时效保存到临时缓存中;
这也是为什么很多社区软件在有分页查询的功能上,越往下越卡的一部分原因;
聚合数据
由于一个点赞动作,会触发以下子任务:
- 这个视频的点赞计数
- 用户对这个视频的点赞记录
- 用户点赞列表刷新
- ...
所以点赞的数据在执行之初采取的就是异步写入+最终一致的数据管理方式;
并且在各个子任务中,也需要设计一个聚合n秒的收集桶用于批处理数据,极大减少IO消耗
系统设计
上图来自文章;
thumbup-job
是所有子任务的调度者,是承上启下的核心任务层,除了下发子任务外,job
的任务也包括数据库的binglod数据同步;
系统设计上并不复杂,业务应用-异步消费-触发子业务,db读上也是采用了常规的分布式服务的解决方案:
图取自原文https://www.bilibili.com/read/cv21576373/
不过作为一个用户强体验的点赞功能,在细节上还是有很多特异性的:
数据高可用
由于存储层有三层存储,二层落库,所以当缓存不可用时,可以从tidb
和 归档层
中获取数据;
并且由于两层都有用户的全量数据,也间接实现了数据分流的设计,这样一来数据的来源来自:
- 缓存redis
- 部分的本地缓存
- tidb
- 归档数据库
当某一层发生异常时,保证数据高可用的同时,在系统逐渐恢复 重启 时还能做到限流、分流的作用;
在原文有这样一段话👆,这让我想到了21年bilibili的一次事故时,视频能打开,并且可以播放,但是里面的评论、弹幕、点赞、硬币等等信息全部都是初始化的状态值;
说明了除了点赞系统,其实大多体现数据的地方都采用了这样返回空值+客户机本地缓存+假特效的方式应对实机崩溃时的状况;
从用户打开app:
- 访问客户机缓存发生有这个视频
- 使用这个视频id请求视频视频,这时视频系统并未崩溃正常返回
- 其余系统,基本信息、点赞、评论都因为某些原因不可用,返回空值;
- 我还是可以乐呵呵的看视频:)
从用户的体验上来说,这种方案是并不可少的;
数据延迟
b站对于分布体系下数据延迟的处理是:服务A直接调用数据写入的服务B接口,thumbup-service-A
调用 thumbup-service-B
关于延迟,在前文中有提到在数据实际落库时会进行一个n秒的数据聚合动作;既然有这种聚合动作,但是必然会出现n秒的这个点赞数是不会发生改变的。
这也是现在b站点赞后马上刷新并不会实时的在另一个用户上体现出+1的动作,并且还有1w+这种体现数据量的邪教设计🐕
所以数据延迟在点赞系统中是可以被允许接收的,也因此服务A调用服务B这种外人看起来很怪的操作,内部肯定也是进行了对应的限流、限量的规则限制;
总结
由于是b站的重度使用者,在读这篇文章前必然的发现了b站的一些体验上的优化;最明显的就是一个视频,3年前的数据与现在的数据相比会发现:
- 弹幕消失
- 点赞数清空
- ....
当然这些是作用在那些特别热门的视频上的优化,除此之外我猜测b站在客户机上使用了大量的本地缓存使我们感觉自己的数据实时化,然而其实存在聚合数据时间的延迟;
点赞功能是一个很简单的功能,相信写过评论功能的大伙都有这种感觉;但是在原文的设计环境下,也是挑战多多,不可小瞧任何一个敌人