天天看点

高并发系统设计(1): 如何实现抖音关注/粉丝列表

作者:赵帅虎

抖音作为国民短视频社交应用,有数据显示总用户数量已超过8亿,每日活跃用户达7亿,人均单日使用时长超过2小时。在这样庞大的用户基数下,一个小小的功能,背后可能需要复杂的设计。以用户服务为例,单单存储8亿用户,已经远远超出单库单表MySQL的极限。本文中我们的问题是如何在这样的用户基数上,实现关注列表、粉丝列表功能。

需求分析

事实上,关注列表、粉丝列表只是需求的一部分。抖音上人与人的关系是一种弱关系,可以单方向进行关注,这一点与微博、twitter类似。作为对比,微信上人与人的关系是强关系,必须是对等的。

如果打开抖音,我们可以看到三个标签:朋友、关注、粉丝。

高并发系统设计(1): 如何实现抖音关注/粉丝列表

这三个标签分别对应:

  1. 查询自己的朋友列表,并支持搜索(互相关注)
  2. 查询自己的关注列表,并支持搜索(关注的用户)
  3. 查询自己的粉丝列表,并支持搜索(关注你的用户)

除了这三类读操作之外,还有一些写操作:

  1. 关注
  2. 取消关注
  3. 拉黑
  4. 取消拉黑等等

数据分布

凭经验来看,每个用户关注的用户数是有限的,大概率不会超过5000; 一个普通用户的粉丝数也是有限的,能超过5000至少说明精心运营过。

但超级大v的粉丝数可以远远超出这个量级,比如在抖音上搜索“刘德华”、“邓紫棋”、“王一博“等,粉丝数都是千万级别的。精心运营过的刘德华粉丝数达到了惊人的7600w,这个量级可以给他定制化一张MySQL表了,表名就叫liudehua_followers

高并发系统设计(1): 如何实现抖音关注/粉丝列表

当然,定制化一张MySQL表只是一个玩笑。但一个事实是,刘德华的粉丝数是我的100w倍;我们没有渠道得知抖音用户的粉丝数分布,马太效应肯定是超级明显。

存储架构

关注关系本质上是一种关系,但业务上体现为两种:粉丝列表和关注列表。朋友是粉丝列表和关注列表的交集。如果设计一种存储架构,需要满足一些条件:

  1. 持久化存储: 关注关系存起来以后,不能丢
  2. 数据一致性: 粉丝列表和关注列表的数据必须一致
  3. 高性能: 查询和更新都必须快,比如内网服务接口响应100ms以内
  4. 架构简单、资源占用可接受

先不考虑数据量,如果用一张MySQL表存储关注列表,那么结构大概是这样的:

1. uid bigint
2. follower_uid bigint
3. status tinyint
4. create_time tiimestamp
5. modify_time tiimestamp
           

在 uid 和 follower_uid 都加上索引,用Go写个服务封装成接口,就可以用了。

但是,这是8亿用户,分库分表肯定是少不了的。一旦做了分库分表,关注列表和粉丝列表都必须分开存储了,因为:

  1. 如果用uid做分片,通过 follower_uid 取关注列表时,数据分散在不同的分片上,读写的效率都会很低,还不支持分页
  2. 如果用 follower_uid 做分片,通过 uid 读取粉丝列表时,会遇到同样的问题

所以,粉丝列表仍然使用上面的表结构,uid是用户ID,follower_uid是粉丝ID,对uid进行分片;

关注列表则采用稍微不同的表结构,uid是用户ID,following_uid是被关注用户的ID,对uid进行分片:

1. uid bigint
2. following_uid bigint
3. status tinyint
4. create_time tiimestamp
5. modify_time tiimestamp
           

按照功能拆表、对表进行分片,这两个操作完事以后,我们满足了持久化和高性能两个指标,但如何保证两张表数据的一致性呢?

我们简单聊一下CAP理论:

  • Consistency 一致性
  • Availablity 可用性
  • Partition Toloerance 分区容错性

在一个大型分布式系统中,这三点不可能同时完成。我们看CAP理论如何应用到粉丝场景。

在粉丝场景中,我们需要纠结的是C和A选哪个。

如果选C,那么就需要依赖分布式锁,保证两张表都写入以后,才返回结果给客户端;这里的缺点很明显: 一是引入外部依赖(分布式锁),锁挂了怎么兜底;而且性能差;

如果选A,就是最终一致性方案。当关注行为被触发时,优先将数据写入“关注表”(没有马太效应、性能可控),通过消费Binlog更新数据到“粉丝表”;

权衡到业务场景的要求、技术实现的成本、服务的稳定性,选A更优。

业务支持

场景1: 朋友列表

  1. 通过uid获取“关注列表”,list<following_uid>
  2. 通过folloing_uid + uid,反查“粉丝列表”

场景2: 通过名字搜索

  1. 先找到粉丝或关注的 uid list
  2. 用 list + 名字搜索user表(或存放用户信息的ElasticSearch)

这两个场景下,如果是普通用户,基本上没啥问题。

如果我是刘德华,场景1倒是没啥问题,场景2如果搜索的是粉丝,那么性能上也会有问题,怎么解决呢?解决办法就是不让大V搜索。

胖客户端的一点联想

客户端和服务端的分界线从来都不是那么清晰,随着历史的演进,

大学那会儿,教科书上会说浏览器是瘦客户端,

上面提到的存储设计,或者上层的接口设计,都是服务端做的事情。抖音需要服务这么大的用户群体,本身已经需要很多机器。每个看起来微不足道的请求,QPS一旦上来,需要的机器数量都不会少。那么有没有一种方法,可以减少机器占用呢?

当然有,由于目前手机的配置普遍都比较高,很多原本在服务端的信息,都可以缓存到手机内置存储里。只需要一定的更新机制,保证服务端和客户端的数据一致即可。不然手机端的app怎么都这么大呢?

高并发系统设计(1): 如何实现抖音关注/粉丝列表

因此,粉丝列表、关注列表这类数据对都可以缓存到App端。粉丝列表的变更从服务端定期拉取更新;关注列表的变更由App端触发,只需要保证服务端接口调用成功后,更新本地缓存即可。

用本地缓存的数据支持复杂的查询,大大压缩了数据量,解决大大表JOIN的问题,模糊搜索玩出花就可以!

继续阅读