字节2面真题-关系链业务

题目

设计实现关注,粉丝列表,且有分页功能
每天1亿的日活量用户
100万的关注和取关修改量

简介

关系链指的是用户A与用户B的关注关系。以关注属性细分,以关注(订阅)为主,还涉及拉黑、悄悄关注、互相关注、特别关注等多种属性或状态

关系链状态机


A和B的状态就5种

数据量少的时候

一张关系表、一张计数表即可满足线上使用
关系表的结构示例
计数表的结构示例


一次mid新增关注fid请求为例

查询mid的计数并加锁,查询fid的计数并加锁;
查询mid->fid的关系并加锁,查询fid->mid的关系并加锁;
根据状态机,在内存计算新关系后,修改mid->fid的关系,修改fid->mid的关系(若有,比如由单向关注变为互关);
修改mid的关注数,修改fid的粉丝数;

上面架构的缺点

中规模可以先分表
一方面,即使做了分表,关系链数据整体规模仍然超出了建议的整体存储容量(TB级);
另一方面,繁重的事务导致mysql无法支撑很高的写流量,在原始的同步写架构下,表现就是关注失败率变高;如果只是单纯地升级到异步写架构,表现就是消息积压,当消息积压持续时间超过临时缓存有效期时,会引起客诉,治标不治本。
使用mysql作为核心存储的“同步”写关系流程图

升级架构

数据存储从mysql迁移到KV存储,逻辑方面把”同步写mysql“改为”异步写KV“。未选择”同步写KV“的原因,一方面是一条关系对应着多条KV记录,而KV不支持事务;另一方面是异步的架构可以扛住可能存在的瞬时大量关注请求。为了兼容订阅了mysql binlog的业务,在“异步写KV”之后还会”异步写mysql“。

在新架构下,对于每一次用户关注请求,投递databus(异步消息)即视为请求成功,mysql binlog只提供给一些对实时性不太敏感的业务方(如数据平台),所以对于异步写mysql事件的偶尔的轻微积压我们并不需要关心,而对实时性要求比较高的业务方,我们在处理完异步写KV事件后,会投递了databus供这些业务订阅。



KV存储最大的优势在于,底层能提供计数(count)方法以替代冗余的mysql计数表,这样的好处是,我们只需要维护一张单纯保存关系的KV表即可。我们设计的存储结构是:

key为{attr|mid}fid,attr为关系拉链类型,mid和fid都表示用户id,{attr|mid}表示拼接attr和mid作为hash,该hash下的多个fid将按照字典序存储,结合KV服务提供的拉链遍历方法(scan)以什么开头的
比如{ATTR_FOLLOW|11}hash出来是AAAA
则查11的关注列表就是遍历AAAA% ,可以获取该hash下的所有的fid;
value为结构体,包含了attribute(关系属性)和mtime(修改时间);

key的解释 (attr和attribute容易混淆)

key中的attr为关系拉链类型,一共有5类(3类正向关系,2类反向关系):ATTR_WHISPER表示悄悄关注类(mid悄悄关注了fid),
ATTR_FOLLOW表示关注类(mid关注了fid),
ATTR_BLACK表示拉黑类(mid拉黑了fid),
ATTR_WHISPERED表示被悄悄关注类(mid被fid悄悄关注了),ATTR_FOLLOWED表示被关注类(mid被fid关注了)。
对用户来说,各类列表和关系链类型的映射关系如下:
关注列表:根据产品需求的不同,大部分时候指关注类关系链(attr=ATTR_FOLLOW),有些场景也会加上悄悄关注类关系链(attr=ATTR_WHISPER);
粉丝列表:被悄悄关注类关系链(attr=ATTR_WHISPERED)和被关注类关系链(attr=ATTR_FOLLOWED)的合集;
黑名单列表:拉黑类关系链(attr=ATTR_BLACK)。
value中的attribute表示当前的关系属性,一共有4种:WHISPER表示悄悄关注、FOLLOW表示关注、FRIEND表示互相关注、BLACK表示拉黑。这里和前文的attr较易混淆,它们之间完整的映射关系如下:
attr=ATTR_WHISPER或ATTR_WHISPERED下可以有attribute=WHISPER;
attr=ATTR_FOLLOW或ATTR_FOLLOWED下可以有attribute=FOLLOW或者FRIEND;
attr=ATTR_BLACK下可以有attribute=BLACK。
midA的五种关系拉链如图:


升级到KV存储后读操作

如果要正向查询mid与fid的关系,只需要点查(get、batch_get)遍历3种正向attr;
如果要查询全量关注关系、黑名单,只需要找对应attr分别执行scan;
如果要查询用户计数,只需要count对应的attr即可;

升级到KV存储后写操作

mysql有事务来保证原子性,而kv存储并不支持事务。而对于用户的请求而言,投递databus即算关注成功,那么在异步处理这条消息时,需要100%保障成功写入,因此我们在处理异步消息时,对每个写入操作都加上了失败无限重试的逻辑。
极端情况下,还可能会遇到写入冲突问题:比如某个时间点用户A关注了用户B,”同时“用户B关注了用户A,此时就可能会引发一些意想不到的数据错误(因为单向关注和互关是两个不同的属性,任一方的关注行为都会影响这个属性)。为了避免这种情况出现,我们利用了消息队列同一个key下数据的有序特性,通过保证同一对用户分配到一个key,保证了同一对用户的操作是有序执行的。
还是以mid新增关注fid为例,对于每一条关注事件:

job需要先put一条正向关注关系,然后进行上限校验,如果超过上限那么回滚退出;
然后批量put因本次关注动作所影响的所有其他反向attr,比如mid的被关注关系(attr=ATTR_WHISPERED)、fid的关注关系(attr=ATTR_FOLLOW,若有,比如由单向关注变为互关);
上述任何一个put操作失败了,都需要重试;直到这些动作都完成了,那么认为此次关注事件成功;
投递databus,告知订阅方发生了关注事件;
投递异步写mysql事件,把关注事件同步mysql,产出binlog供订阅方使用。

全量关注列表、全量黑名单

线上查询请求中,有一定比例是查询全量关注列表、全量黑名单。上一节中提到,为了不冗余存储一份关系链计数,KV的存储设计得比较特殊,一个用户的正向关注关系分布在3个不同的attr(即3个不同的关系拉链)里。如果想从KV存储拉取一个用户的全量关系列表,那么同时需要分别对3种正向关系拉链都做循环scan(因为每次scan有数量上限),但由于scan方法性能相对较差,所以需要在KV存储的上层加一套缓存,通过降低回源比例严格控制scan QPS。
鉴于memcached对大key有比较好的性能,前人在KV存储的上层加了一个memcached缓存,用于存储用户的全量关系列表,具体业务流程如下


Memcached是一个开源的高性能分布式内存对象缓存系统。它可以帮助应用程序加快访问数据库、API调用或其他耗时操作的速度,通过将数据存储在内存中,避免了频繁的磁盘读写操作。
Memcached使用简单的键值对存储模型,可以将数据以键值对的形式存储在内存中,并提供快速的读写访问。它常用于缓存常用的数据库查询结果、网页内容、API响应等。
Memcached是一个分布式系统,它可以在多台服务器上运行,并通过哈希算法将数据分散存储在不同的节点上。这样可以提高系统的可扩展性和容错性,确保即使某个节点失效,数据仍然可用。
Memcached支持多种编程语言,并且有许多客户端库可供开发人员使用。它被广泛应用于Web开发、缓存加速、数据分析等领域,为应用程序提供了高速的数据访问能力。

查询用户和其他一个或多个用户的关系


如果每次都从memcached拉全量关系列表然后内存中取交集,网络的开销会非常大,因此对这种查询场景,也需要设计一套适用于点查的缓存。

活跃用户的关注数一般都在几十到几百的区间,用于点查的缓存不需要严格有序,但要支持指定hashkey的查询,redis hash和其提供的hget、hmget、hset、hmset方法都是非常适合这一场景的。因此查询层缓存设计如下:key为mid,hashkey为mid有关系的每一个用户id,value为他们的关系数据,和前面midA在KV存储的数据对应如下:

Redis中的Hash是一种用于存储键值对的数据结构,其中键是唯一的,值可以是字符串、数字等。Hash是一种适用于存储对象的数据结构,可以实现对对象的部分修改和读取。
Redis提供了一系列操作Hash的命令,包括hget、hmget、hset、hmset等。

  1. hget命令用于获取Hash中指定字段的值:
    Jedis jedis = new Jedis("localhost");
    String value = jedis.hget("hashKey", "field");
    其中,"hashKey"是Hash的键,"field"是要获取的字段名,返回的是字段对应的值。
  2. hmget命令用于获取Hash中多个字段的值:
    Jedis jedis = new Jedis("localhost");
    List<String> values = jedis.hmget("hashKey", "field1", "field2");
    其中,"hashKey"是Hash的键,"field1"、"field2"是要获取的字段名,返回的是一个列表,包含了字段对应的值。
  3. hset命令用于设置Hash中指定字段的值:
    Jedis jedis = new Jedis("localhost");
    Long result = jedis.hset("hashKey", "field", "value");
    其中,"hashKey"是Hash的键,"field"是要设置的字段名,"value"是要设置的字段值,返回的是设置操作的结果(1表示成功,0表示字段已存在)。
  4. hmset命令用于设置Hash中多个字段的值:
    Jedis jedis = new Jedis("localhost");
    String result = jedis.hmset("hashKey", Map.of("field1", "value1", "field2", "value2"));
    其中,"hashKey"是Hash的键,Map中的键值对表示要设置的字段和对应的值,返回的是设置操作的结果。
    需要注意的是,以上示例中使用的是Jedis客户端进行操作,你可以根据自己的需求选择其他的Redis客户端库。

redis hash缓存中关注关系的存储结构



由于hash里保存的是midA的全部正向关注关系,当缓存miss需要回源时,要获取全量关注关系,可以和前面的memcached配合使用,业务流程如下:

redis hash架构下的一对多关系的查询业务流程

热点场景

问题根因:集中在Redis的少数几台实例上,木桶短板效应就会非常明显。
对于热点用户,在请求Redis前会先查询本地Localcache(Localcache中存储的up主关系列表数据,隔十几秒更新一次)。虽然在这十几秒内可能会存在数据不一致的情况,但从实际业务角度看,引发热点请求的都是大up主,这些up主的关系列表较少发生变动,因此几乎不会对用户体验造成影响。

原文

https://mp.weixin.qq.com/s/8q_IstzfxUkkPZ8BQvsXNQ

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容