字節(jié)2面真題-關(guān)系鏈業(yè)務(wù)

題目

設(shè)計(jì)實(shí)現(xiàn)關(guān)注评甜,粉絲列表,且有分頁(yè)功能
每天1億的日活量用戶
100萬(wàn)的關(guān)注和取關(guān)修改量

簡(jiǎn)介

關(guān)系鏈指的是用戶A與用戶B的關(guān)注關(guān)系。以關(guān)注屬性細(xì)分饲常,以關(guān)注(訂閱)為主,還涉及拉黑狼讨、悄悄關(guān)注贝淤、互相關(guān)注、特別關(guān)注等多種屬性或狀態(tài)

關(guān)系鏈狀態(tài)機(jī)


A和B的狀態(tài)就5種

數(shù)據(jù)量少的時(shí)候

一張關(guān)系表政供、一張計(jì)數(shù)表即可滿足線上使用
關(guān)系表的結(jié)構(gòu)示例
計(jì)數(shù)表的結(jié)構(gòu)示例


一次mid新增關(guān)注fid請(qǐng)求為例

查詢mid的計(jì)數(shù)并加鎖播聪,查詢fid的計(jì)數(shù)并加鎖;
查詢mid->fid的關(guān)系并加鎖鲫骗,查詢fid->mid的關(guān)系并加鎖犬耻;
根據(jù)狀態(tài)機(jī),在內(nèi)存計(jì)算新關(guān)系后执泰,修改mid->fid的關(guān)系枕磁,修改fid->mid的關(guān)系(若有,比如由單向關(guān)注變?yōu)榛リP(guān))术吝;
修改mid的關(guān)注數(shù)计济,修改fid的粉絲數(shù)茸苇;

上面架構(gòu)的缺點(diǎn)

中規(guī)模可以先分表
一方面沦寂,即使做了分表学密,關(guān)系鏈數(shù)據(jù)整體規(guī)模仍然超出了建議的整體存儲(chǔ)容量(TB級(jí));
另一方面传藏,繁重的事務(wù)導(dǎo)致mysql無(wú)法支撐很高的寫(xiě)流量腻暮,在原始的同步寫(xiě)架構(gòu)下,表現(xiàn)就是關(guān)注失敗率變高毯侦;如果只是單純地升級(jí)到異步寫(xiě)架構(gòu)哭靖,表現(xiàn)就是消息積壓,當(dāng)消息積壓持續(xù)時(shí)間超過(guò)臨時(shí)緩存有效期時(shí)侈离,會(huì)引起客訴试幽,治標(biāo)不治本。
使用mysql作為核心存儲(chǔ)的“同步”寫(xiě)關(guān)系流程圖

升級(jí)架構(gòu)

數(shù)據(jù)存儲(chǔ)從mysql遷移到KV存儲(chǔ)卦碾,邏輯方面把”同步寫(xiě)mysql“改為”異步寫(xiě)KV“铺坞。未選擇”同步寫(xiě)KV“的原因,一方面是一條關(guān)系對(duì)應(yīng)著多條KV記錄洲胖,而KV不支持事務(wù)济榨;另一方面是異步的架構(gòu)可以扛住可能存在的瞬時(shí)大量關(guān)注請(qǐng)求。為了兼容訂閱了mysql binlog的業(yè)務(wù)宾濒,在“異步寫(xiě)KV”之后還會(huì)”異步寫(xiě)mysql“腿短。

在新架構(gòu)下,對(duì)于每一次用戶關(guān)注請(qǐng)求绘梦,投遞databus(異步消息)即視為請(qǐng)求成功,mysql binlog只提供給一些對(duì)實(shí)時(shí)性不太敏感的業(yè)務(wù)方(如數(shù)據(jù)平臺(tái))赴魁,所以對(duì)于異步寫(xiě)mysql事件的偶爾的輕微積壓我們并不需要關(guān)心卸奉,而對(duì)實(shí)時(shí)性要求比較高的業(yè)務(wù)方,我們?cè)谔幚硗戤惒綄?xiě)KV事件后颖御,會(huì)投遞了databus供這些業(yè)務(wù)訂閱榄棵。



KV存儲(chǔ)最大的優(yōu)勢(shì)在于,底層能提供計(jì)數(shù)(count)方法以替代冗余的mysql計(jì)數(shù)表潘拱,這樣的好處是疹鳄,我們只需要維護(hù)一張單純保存關(guān)系的KV表即可。我們?cè)O(shè)計(jì)的存儲(chǔ)結(jié)構(gòu)是:

key為{attr|mid}fid芦岂,attr為關(guān)系拉鏈類型瘪弓,mid和fid都表示用戶id,{attr|mid}表示拼接attr和mid作為hash禽最,該hash下的多個(gè)fid將按照字典序存儲(chǔ)腺怯,結(jié)合KV服務(wù)提供的拉鏈遍歷方法(scan)以什么開(kāi)頭的
比如{ATTR_FOLLOW|11}hash出來(lái)是AAAA
則查11的關(guān)注列表就是遍歷AAAA% 袱饭,可以獲取該hash下的所有的fid;
value為結(jié)構(gòu)體呛占,包含了attribute(關(guān)系屬性)和mtime(修改時(shí)間)虑乖;

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

key中的attr為關(guān)系拉鏈類型,一共有5類(3類正向關(guān)系晾虑,2類反向關(guān)系):ATTR_WHISPER表示悄悄關(guān)注類(mid悄悄關(guān)注了fid)疹味,
ATTR_FOLLOW表示關(guān)注類(mid關(guān)注了fid),
ATTR_BLACK表示拉黑類(mid拉黑了fid)帜篇,
ATTR_WHISPERED表示被悄悄關(guān)注類(mid被fid悄悄關(guān)注了)佛猛,ATTR_FOLLOWED表示被關(guān)注類(mid被fid關(guān)注了)。
對(duì)用戶來(lái)說(shuō)坠狡,各類列表和關(guān)系鏈類型的映射關(guān)系如下:
關(guān)注列表:根據(jù)產(chǎn)品需求的不同继找,大部分時(shí)候指關(guān)注類關(guān)系鏈(attr=ATTR_FOLLOW),有些場(chǎng)景也會(huì)加上悄悄關(guān)注類關(guān)系鏈(attr=ATTR_WHISPER)逃沿;
粉絲列表:被悄悄關(guān)注類關(guān)系鏈(attr=ATTR_WHISPERED)和被關(guān)注類關(guān)系鏈(attr=ATTR_FOLLOWED)的合集婴渡;
黑名單列表:拉黑類關(guān)系鏈(attr=ATTR_BLACK)。
value中的attribute表示當(dāng)前的關(guān)系屬性凯亮,一共有4種:WHISPER表示悄悄關(guān)注边臼、FOLLOW表示關(guān)注、FRIEND表示互相關(guān)注假消、BLACK表示拉黑柠并。這里和前文的attr較易混淆,它們之間完整的映射關(guān)系如下:
attr=ATTR_WHISPER或ATTR_WHISPERED下可以有attribute=WHISPER富拗;
attr=ATTR_FOLLOW或ATTR_FOLLOWED下可以有attribute=FOLLOW或者FRIEND臼予;
attr=ATTR_BLACK下可以有attribute=BLACK。
midA的五種關(guān)系拉鏈如圖:


升級(jí)到KV存儲(chǔ)后讀操作

如果要正向查詢mid與fid的關(guān)系啃沪,只需要點(diǎn)查(get粘拾、batch_get)遍歷3種正向attr;
如果要查詢?nèi)筷P(guān)注關(guān)系创千、黑名單缰雇,只需要找對(duì)應(yīng)attr分別執(zhí)行scan;
如果要查詢用戶計(jì)數(shù)追驴,只需要count對(duì)應(yīng)的attr即可械哟;

升級(jí)到KV存儲(chǔ)后寫(xiě)操作

mysql有事務(wù)來(lái)保證原子性,而kv存儲(chǔ)并不支持事務(wù)殿雪。而對(duì)于用戶的請(qǐng)求而言暇咆,投遞databus即算關(guān)注成功,那么在異步處理這條消息時(shí)冠摄,需要100%保障成功寫(xiě)入糯崎,因此我們?cè)谔幚懋惒较r(shí)几缭,對(duì)每個(gè)寫(xiě)入操作都加上了失敗無(wú)限重試的邏輯。
極端情況下沃呢,還可能會(huì)遇到寫(xiě)入沖突問(wèn)題:比如某個(gè)時(shí)間點(diǎn)用戶A關(guān)注了用戶B年栓,”同時(shí)“用戶B關(guān)注了用戶A,此時(shí)就可能會(huì)引發(fā)一些意想不到的數(shù)據(jù)錯(cuò)誤(因?yàn)閱蜗蜿P(guān)注和互關(guān)是兩個(gè)不同的屬性薄霜,任一方的關(guān)注行為都會(huì)影響這個(gè)屬性)某抓。為了避免這種情況出現(xiàn),我們利用了消息隊(duì)列同一個(gè)key下數(shù)據(jù)的有序特性惰瓜,通過(guò)保證同一對(duì)用戶分配到一個(gè)key否副,保證了同一對(duì)用戶的操作是有序執(zhí)行的。
還是以mid新增關(guān)注fid為例崎坊,對(duì)于每一條關(guān)注事件:

job需要先put一條正向關(guān)注關(guān)系备禀,然后進(jìn)行上限校驗(yàn),如果超過(guò)上限那么回滾退出奈揍;
然后批量put因本次關(guān)注動(dòng)作所影響的所有其他反向attr曲尸,比如mid的被關(guān)注關(guān)系(attr=ATTR_WHISPERED)、fid的關(guān)注關(guān)系(attr=ATTR_FOLLOW男翰,若有另患,比如由單向關(guān)注變?yōu)榛リP(guān));
上述任何一個(gè)put操作失敗了蛾绎,都需要重試昆箕;直到這些動(dòng)作都完成了,那么認(rèn)為此次關(guān)注事件成功租冠;
投遞databus鹏倘,告知訂閱方發(fā)生了關(guān)注事件;
投遞異步寫(xiě)mysql事件肺稀,把關(guān)注事件同步mysql第股,產(chǎn)出binlog供訂閱方使用。

全量關(guān)注列表话原、全量黑名單

線上查詢請(qǐng)求中,有一定比例是查詢?nèi)筷P(guān)注列表诲锹、全量黑名單繁仁。上一節(jié)中提到,為了不冗余存儲(chǔ)一份關(guān)系鏈計(jì)數(shù)归园,KV的存儲(chǔ)設(shè)計(jì)得比較特殊黄虱,一個(gè)用戶的正向關(guān)注關(guān)系分布在3個(gè)不同的attr(即3個(gè)不同的關(guān)系拉鏈)里。如果想從KV存儲(chǔ)拉取一個(gè)用戶的全量關(guān)系列表庸诱,那么同時(shí)需要分別對(duì)3種正向關(guān)系拉鏈都做循環(huán)scan(因?yàn)槊看蝧can有數(shù)量上限)捻浦,但由于scan方法性能相對(duì)較差晤揣,所以需要在KV存儲(chǔ)的上層加一套緩存,通過(guò)降低回源比例嚴(yán)格控制scan QPS朱灿。
鑒于memcached對(duì)大key有比較好的性能昧识,前人在KV存儲(chǔ)的上層加了一個(gè)memcached緩存,用于存儲(chǔ)用戶的全量關(guān)系列表盗扒,具體業(yè)務(wù)流程如下


Memcached是一個(gè)開(kāi)源的高性能分布式內(nèi)存對(duì)象緩存系統(tǒng)跪楞。它可以幫助應(yīng)用程序加快訪問(wèn)數(shù)據(jù)庫(kù)、API調(diào)用或其他耗時(shí)操作的速度侣灶,通過(guò)將數(shù)據(jù)存儲(chǔ)在內(nèi)存中甸祭,避免了頻繁的磁盤(pán)讀寫(xiě)操作。
Memcached使用簡(jiǎn)單的鍵值對(duì)存儲(chǔ)模型褥影,可以將數(shù)據(jù)以鍵值對(duì)的形式存儲(chǔ)在內(nèi)存中池户,并提供快速的讀寫(xiě)訪問(wèn)。它常用于緩存常用的數(shù)據(jù)庫(kù)查詢結(jié)果凡怎、網(wǎng)頁(yè)內(nèi)容校焦、API響應(yīng)等。
Memcached是一個(gè)分布式系統(tǒng)栅贴,它可以在多臺(tái)服務(wù)器上運(yùn)行斟湃,并通過(guò)哈希算法將數(shù)據(jù)分散存儲(chǔ)在不同的節(jié)點(diǎn)上。這樣可以提高系統(tǒng)的可擴(kuò)展性和容錯(cuò)性檐薯,確保即使某個(gè)節(jié)點(diǎn)失效凝赛,數(shù)據(jù)仍然可用。
Memcached支持多種編程語(yǔ)言坛缕,并且有許多客戶端庫(kù)可供開(kāi)發(fā)人員使用墓猎。它被廣泛應(yīng)用于Web開(kāi)發(fā)、緩存加速赚楚、數(shù)據(jù)分析等領(lǐng)域毙沾,為應(yīng)用程序提供了高速的數(shù)據(jù)訪問(wèn)能力。

查詢用戶和其他一個(gè)或多個(gè)用戶的關(guān)系


如果每次都從memcached拉全量關(guān)系列表然后內(nèi)存中取交集宠页,網(wǎng)絡(luò)的開(kāi)銷會(huì)非常大左胞,因此對(duì)這種查詢場(chǎng)景,也需要設(shè)計(jì)一套適用于點(diǎn)查的緩存举户。

活躍用戶的關(guān)注數(shù)一般都在幾十到幾百的區(qū)間烤宙,用于點(diǎn)查的緩存不需要嚴(yán)格有序,但要支持指定hashkey的查詢俭嘁,redis hash和其提供的hget躺枕、hmget、hset、hmset方法都是非常適合這一場(chǎng)景的拐云。因此查詢層緩存設(shè)計(jì)如下:key為mid罢猪,hashkey為mid有關(guān)系的每一個(gè)用戶id,value為他們的關(guān)系數(shù)據(jù)叉瘩,和前面midA在KV存儲(chǔ)的數(shù)據(jù)對(duì)應(yīng)如下:

Redis中的Hash是一種用于存儲(chǔ)鍵值對(duì)的數(shù)據(jù)結(jié)構(gòu)膳帕,其中鍵是唯一的,值可以是字符串房揭、數(shù)字等备闲。Hash是一種適用于存儲(chǔ)對(duì)象的數(shù)據(jù)結(jié)構(gòu),可以實(shí)現(xiàn)對(duì)對(duì)象的部分修改和讀取捅暴。
Redis提供了一系列操作Hash的命令恬砂,包括hget、hmget蓬痒、hset泻骤、hmset等。

  1. hget命令用于獲取Hash中指定字段的值:
    Jedis jedis = new Jedis("localhost");
    String value = jedis.hget("hashKey", "field");
    其中梧奢,"hashKey"是Hash的鍵狱掂,"field"是要獲取的字段名,返回的是字段對(duì)應(yīng)的值亲轨。
  2. hmget命令用于獲取Hash中多個(gè)字段的值:
    Jedis jedis = new Jedis("localhost");
    List<String> values = jedis.hmget("hashKey", "field1", "field2");
    其中趋惨,"hashKey"是Hash的鍵,"field1"惦蚊、"field2"是要獲取的字段名器虾,返回的是一個(gè)列表,包含了字段對(duì)應(yīng)的值蹦锋。
  3. hset命令用于設(shè)置Hash中指定字段的值:
    Jedis jedis = new Jedis("localhost");
    Long result = jedis.hset("hashKey", "field", "value");
    其中兆沙,"hashKey"是Hash的鍵,"field"是要設(shè)置的字段名莉掂,"value"是要設(shè)置的字段值葛圃,返回的是設(shè)置操作的結(jié)果(1表示成功,0表示字段已存在)憎妙。
  4. hmset命令用于設(shè)置Hash中多個(gè)字段的值:
    Jedis jedis = new Jedis("localhost");
    String result = jedis.hmset("hashKey", Map.of("field1", "value1", "field2", "value2"));
    其中库正,"hashKey"是Hash的鍵,Map中的鍵值對(duì)表示要設(shè)置的字段和對(duì)應(yīng)的值厘唾,返回的是設(shè)置操作的結(jié)果诀诊。
    需要注意的是,以上示例中使用的是Jedis客戶端進(jìn)行操作阅嘶,你可以根據(jù)自己的需求選擇其他的Redis客戶端庫(kù)。

redis hash緩存中關(guān)注關(guān)系的存儲(chǔ)結(jié)構(gòu)



由于hash里保存的是midA的全部正向關(guān)注關(guān)系,當(dāng)緩存miss需要回源時(shí)讯柔,要獲取全量關(guān)注關(guān)系抡蛙,可以和前面的memcached配合使用,業(yè)務(wù)流程如下:

redis hash架構(gòu)下的一對(duì)多關(guān)系的查詢業(yè)務(wù)流程

熱點(diǎn)場(chǎng)景

問(wèn)題根因:集中在Redis的少數(shù)幾臺(tái)實(shí)例上魂迄,木桶短板效應(yīng)就會(huì)非常明顯粗截。
對(duì)于熱點(diǎn)用戶,在請(qǐng)求Redis前會(huì)先查詢本地Localcache(Localcache中存儲(chǔ)的up主關(guān)系列表數(shù)據(jù)捣炬,隔十幾秒更新一次)熊昌。雖然在這十幾秒內(nèi)可能會(huì)存在數(shù)據(jù)不一致的情況,但從實(shí)際業(yè)務(wù)角度看湿酸,引發(fā)熱點(diǎn)請(qǐng)求的都是大up主婿屹,這些up主的關(guān)系列表較少發(fā)生變動(dòng),因此幾乎不會(huì)對(duì)用戶體驗(yàn)造成影響推溃。

原文

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末昂利,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子铁坎,更是在濱河造成了極大的恐慌蜂奸,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件硬萍,死亡現(xiàn)場(chǎng)離奇詭異扩所,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)朴乖,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門(mén)祖屏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人寒砖,你說(shuō)我怎么就攤上這事赐劣。” “怎么了哩都?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵魁兼,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我漠嵌,道長(zhǎng)咐汞,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任儒鹿,我火速辦了婚禮化撕,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘约炎。我一直安慰自己植阴,他們只是感情好蟹瘾,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著掠手,像睡著了一般憾朴。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上喷鸽,一...
    開(kāi)封第一講書(shū)人閱讀 51,301評(píng)論 1 301
  • 那天众雷,我揣著相機(jī)與錄音,去河邊找鬼做祝。 笑死砾省,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的混槐。 我是一名探鬼主播编兄,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼纵隔!你這毒婦竟也來(lái)了翻诉?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤捌刮,失蹤者是張志新(化名)和其女友劉穎碰煌,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體绅作,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡芦圾,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了俄认。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片个少。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖眯杏,靈堂內(nèi)的尸體忽然破棺而出夜焦,到底是詐尸還是另有隱情,我是刑警寧澤岂贩,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布茫经,位于F島的核電站,受9級(jí)特大地震影響萎津,放射性物質(zhì)發(fā)生泄漏卸伞。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一锉屈、第九天 我趴在偏房一處隱蔽的房頂上張望荤傲。 院中可真熱鬧,春花似錦颈渊、人聲如沸遂黍。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)妓湘。三九已至查蓉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間榜贴,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工妹田, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留唬党,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓鬼佣,卻偏偏與公主長(zhǎng)得像驶拱,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子晶衷,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容