1. 程式人生 > >涼了呀,面試官叫我設計一個排行榜。

涼了呀,面試官叫我設計一個排行榜。

這是why哥的第89篇原創文章

前兩天,有一個讀者給我發了一張圖片。

我問:發什麼腎麼事了?

於是有了這樣的對話:

他發的圖,就是微信運動步數排行榜的截圖:

其實扯了這麼多,這就是個常見的面試場景題:如何設計一個排行榜?

這個題吧,其實就是考你面試準備範圍的廣度,見過就會答,沒見過...就難說了。

當然,如果你在實際業務中做過排行榜,那麼這題正中下懷,你也不要笑出聲來,場景題面試官是會給你思考時間的。

所以你不要張口就來,你只需要眉頭稍稍一皺,給面試官說:這題我想想啊。

然後稍微組織一下語言,說出來就行。

這次的文章,就帶著大家分析一下“排行榜”這個場景題,到底應該怎麼做。

基於資料庫

這個題,如果是真的之前沒有遇見過,可能最容易進入大家視野的就是平時接觸的最多的資料庫了。

因為一想到“排行榜”,就想到了 order by。

一想了 order by,就想到了資料庫。

一想到了資料庫...

兄弟,你路就走窄了。

雖然我曾經就基於 MySQL 做過排行榜,因為當時是為了一個比賽臨時搭建的服務,根本就沒有引入 Redis。我評估了一下搭建 Redis 的時間和用 MySQL 直接開發的時間。

於是選擇了 MySQL。

而讓我選擇 MySQL 的根本原因還是我已經知道進入決賽的隊伍只有 10 支,也就是說我的排行榜表裡面從始至終也只有 10 條資料。

選手提交程式碼之後,系統實時算分,然後更新排行榜表。

然後介面返回給前端頁面下面這些資料,而下面這些資料都在一個表裡面:

  • 隊伍按照歷史最高分數排名
  • 隊伍名稱
  • 歷史最高分數
  • 最近一次提交得分
  • 最近一次提交時間

前端每隔一分鐘呼叫我的介面,相同分數,名次相同,所以我在接口裡面用一條比較複雜的 sql 去查詢資料庫,上面的這些欄位就都有了。

你看,排行榜確實是可以用 MySQL 來做的。

不一定非得上 Redis,記住一句話:脫離業務場景的方案設計,都是耍流氓。

但是這玩意和“萬物皆物件”一樣,別對著面試官說,這一定不是面試官想要聽到的答案。

或者說,這只是想要聽到的一部分回答。

這個回答能用的原因是我給了一個具體的場景,使用者量非常的小,怎麼玩都可以。

甚至我們不借助 MySQL 的排序,把資料查出來,在記憶體裡面排序都可以。

但是如果,這是一個遊戲排行榜,隨著遊戲玩家的增加,達到千萬使用者級別的話,這個方案肯定是不行了。

當然,也許你會給我扯什麼查詢慢就加索引,資料量大就分庫分表的方案。

怎麼說呢,上面這句話是沒有錯的。

但是一旦資料量大起來了,這個方案其實就不是一個特別好的方案。

這問題,得從根上治理。

基於 Redis

這個場景其實就是在考察你對於 Redis 的 sorted set 資料結構的掌握。

sorted set,見名知意,就是有序集合的意思。

在 Redis 中它大概是長這樣的:

前面的 sport:ranking:20210227 是 Redis 中的 key。

value 是一個集合,且可以看出這個集合是有序的。集合中的每一個 member 都有一個 score,然後按照這個 score 進行降序排序。

需要注意的是,圖片中的 score/member 不是我隨便寫的,官網上就是這樣定義的:

https://redis.io/commands/zadd#sorted-sets-101

而且官網上說的是: score / member pairs。

所以我畫圖的時候,score 在前,member 在後。這可不是隨便畫的,雖然誰前誰後好像也不影響什麼玩意。

另一個需要注意的點是,雖然我的示意圖中沒有體現出來,但是在有序集合中,元素即 member 是不可以重複的,但是 score 是可以重複的。

這個很好理解,就比如 20210227 這一天的微信步數,我可以走 6666 步,你也可以走 6666 步,這個是不衝突:

但是,問題就隨之而來了:當 member 的 score 一樣的時候,member 是怎麼排序的呢?

看一下來自官網的答案:

當多個元素具有相同的分數時,它們按照 lexicographically 進行排序。

哎呀,lexicographically 這個單詞不認識。

不慌,你知道的 why哥還兼職教英文:

當分數一樣的時候,按照字典序排序,所以上面的示意圖 jay 在 why 之前。

接下來,看一下有序集合的操作函式,一共有 32 個:

我這裡就不一個個的做 API 教學了,官網上已經寫的很清楚了,如果對於不熟悉的命令,可以去官網上檢視,都是有示例程式碼的。

https://redis.io/commands/zadd#sorted-sets-101

比如這個 ZADD 方法:

為了後面分享的順利進行,我這裡只講幾個需要用到的操作:

  • 新增 member 命令格式:zadd key score member [score member ...]
  • 增加 member 的 score 命令格式:zincrby key increment member
  • 獲取 member 排名命令格式:zrank/zrevrank key member
  • 返回指定排名範圍內的 member 命令格式:zrange/zrevrange key start end [withscores]

先看第一個:新增 member。

比如我們把示意圖中的資料新增到到有序集合裡面去,語法是這樣的:

  • zadd key score member [score member ...]

意思是可以一次新增一對或者多對 score-member,比如下面這兩個命令:

  • zadd sport:ranking:20210227 10026 why
  • zadd sport:ranking:20210227 10158 mx 30169 les 48858 skr 66079 jay

執行之後,返回的數字代表新增成功的 member 個數。

我用專門操作 Redis 的 RDM 視覺化工具來檢視插入的資料,和我自己畫的示意圖相差無幾:

接著看第二個:增加 member 的 score

微信運動排行榜的資料是實時更新的。

目前 member 為 why 的步數是 10268,假設我吃完晚飯出門跑步去了,又跑了 5000 步。

這時得更新我的步數,就用 zincrby 命令,語法是這樣的:

  • zincrby key increment member

對應上面場景的執行命令是這樣的:

  • zincrby sport:ranking:20210227 5000 why

執行完成後,會返回 why 的步數,可以看到從 10026 變成了 15026 :

同時由於我的步數增加,按照 score 倒序,也導致了排序的變化:

所以我們只需要更新 score 就行了,至於排名的變化,Redis 會幫忙保證的。

然後看第三個命令:獲取 member 排名

語法是這樣的:

  • 獲取 member 排名:zrank key member
  • 獲取 member 排名:zrevrank key member

首先,排名都是 0 開始計算的。

zrank 是按照分數從低到高返回 member 排名。

zrevrank 是按照分數從高到低返回 member 排名。

比如現在要獲取 jay 的排名,用 zrank 返回結果就是 4。

  • zrank sport:ranking:20210227 jay

當用 zrevrank 時,jay 的排名就是 0:

  • zrevrank sport:ranking:20210227 jay

所以,在微信步數排行榜的這個需求中,步數越多排名越靠前,我們應該用 zrevrank。

第四個需要掌握的命令是:返回指定排名範圍內的 member。

  • zrange/zrevrange key start end [withscores] 返回指定排名範圍內的 member

這個命令就很關鍵了。

zrange 是按照 score 從低到高返回指定排名範圍內的 member。

zrevrange 是按照 score 從高到低返回指定排名範圍內的 member。

在這裡,我只演示 zrevrange 的命令。

比如我要獲取步數排名前三的 member:

  • zrevrange sport:ranking:20210227 0 2

這個命令有個可選引數:withscores

當帶上這個引數之後,會返回對應 member 的 score:

你想,這不就是排行榜 top N 的場景嗎?

假設我現在要獲取所有使用者的排名,怎麼寫呢?

如下:

  • zrevrange sport:ranking:20210227 0 -1

這就是當前的微信步數排行榜,jay 步數最多,mx 步數最少。

咦,怎麼回事,排行榜好久就出來了呢?

你想想,講完幾個 API 操作,好像功能就實現了呢?

是的,確實是這樣的,甚至我們只需要這兩個 API 就能完成排行榜的需求:

  • zadd key score member [score member ...] 新增 member
  • zrange/zrevrange key start end [withscores] 返回指定排名範圍內的 member

好了,如果大家喜歡的話,感謝大家一鍵三連。本次的文章就到這裡了...

那是不可能的。

索然無味的 API 文章多沒有意思啊。

雖然前面的部分我們已經可以基於 Redis 的有序集合加上幾個簡單的命令,就可以實現排行榜需求了。

但是前面只是鋪墊,接下來,好戲才剛剛開始。

再次審視排行榜

上面的微信步數排行榜有個問題,你發現了嗎?

就上面這個場景而言,所有人來看,看到的都是這樣的排序:

而真實情況是,每個人看見的資料排行資料來源自己的微信好友,而微信好友各不相同,所以看到的排行榜也各不相同。

這個特性,我們並沒有體現出來。

我們上面的場景更加類似於遊戲排行榜,所有的人看到的全服排行榜都是一樣的。

那麼怎麼保證我們每個人看到的各不相同呢?

你思考一下,該從什麼角度去解決這個問題呢?

有序集合的 key 不同,就獲取到不同的 value 集合。

我們當前的 key 是 sport:ranking:20210227,裡面只包含了某一天的資訊。

只要我們在 key 裡面加上使用者的屬性就可以了,假設我的微訊號是 why。

那麼 key 可以設計為這樣 sport:ranking:why:20210227。

這樣,由於 key 裡面多了使用者資訊,每個人的 key 都各不相同,就像這樣的:

對應的命令如下:

  • zadd sport:ranking:why:20210227 10026 why 10158 mx 30169 les 48858 skr 66079 jay
  • zadd sport:ranking:mx:20210227 7688 趙四 9688 劉能 10026 why 10158 mx 54367 大腳

why 和 mx 看到的都是各自好友某一天的微信步數排行榜。

只要把 key 設計好了,這個問題就迎刃而解了。

但是你仔細思考一下,真的就迎刃而解了嗎?

這個問題,我在寫第一版的時候可能是被豬油矇蔽了雙眼,沒發現。

有種“只緣身在此山中”的味道,一心想著 Redis 了。

你想,如果每個使用者都有在redis有一個自己的排行榜,一個使用者的分數更新的時候就需要對所有好友的zset更新,這多大的代價啊,對吧?

當以使用者為緯度做排行榜的時候,就會出現排行榜巨多的情況,導致維護成本升高。

Redis能做,但不是最佳方案。

那麼用什麼方案去做呢?

我提個思路吧:

每個使用者看到的排行榜不一樣,我們其實不用時時刻刻幫使用者維護好排行榜。

維護好了,使用者還不一定來看,出力不討好的節奏。

所以還不如延遲到使用者請求的階段。

當用戶請求檢視排行榜的時候,再去根據使用者的好友關係,迴圈獲取好友的步數,生成排行榜。

具體方案,大家自己思考一下吧。

另外多說一嘴,前段時間不是微信支援了修改微訊號嗎,贏得一大片叫好聲。

其實我當時認真的想了一下,從技術上的實現來說這個需求到底有多難。

我不知道有沒有歷史技術債務在裡面。

但是就說當前這個場景,key 裡面包含了微訊號,注意是微訊號,不是微信暱稱。

因為在設計之初,產品打包票說:放心,微訊號絕對全域性唯一,一旦確定,不可變更。

結果呢,現在要變化了。

產品屁顛屁顛的說:怎麼實現我不管,這個需求使用者呼籲很大,趕緊上線。

你說,對這些類似場景的衝擊有多大?

其實衝擊也不算特別大,一個欄位的變化而已。

但是,微信 14 億使用者啊。

一個簡單的需求,涉及到這個體量之後,就一句話:

量變引起質變。

好了,好了,扯遠了。說回來。

當我把目光再次放到微信排行榜上的時候,我發現,其實我只是給了一個閹割版的排行榜。

是的,我們現在可以獲取到 why 的當前步數是 1680 步,當前排名是 814 名。

比如還是沿用上面的例子,假設現在要獲取我的微信好友 jay 的微信步數排行榜情況。

先獲取 jay 的名次:

  • zrevrank sport:ranking:why:20210227 jay

名次為 0,程式裡面可以對其進行加一操作。就是第一名了。

接著獲取 jay 的今日步數:

  • zscore sport:ranking:why:20210227 jay

66079,步數也有了。

現在我們知道了:why 的好友 jay 今日運動步數 66079 步,在 why 的微信好友中排第一名。

但是你仔細看,這上面我還漏了兩個欄位:

  • 微信頭像
  • 朋友點贊個數

兩個欄位應該怎麼放呢?

放資料庫裡面當然可以,但是我們主要還是說一下 Redis 的解決方案。

這個時候其實我們想要儲存的是 User 物件,物件裡面有這幾個欄位:暱稱、頭像圖片連結、點贊數、步數。

你說,這個用 Redis 的啥資料結構來存?

可不就得用 Hash 結構了嗎。

Hash 結構同樣涉及到 key 和 value,那麼它們分別是什麼呢?

key 就是我們的有序集合的 key 後面再加上好友暱稱,比如這樣的:

對應的命令是這樣的:

  • hmset sport:ranking:why:20210227:jay nickName jay headPhoto xxx likeNum 520 walkNum 66079

執行完成之後,在 RDM 裡面看起來是這樣的:

當後續有更多的讚的時候,需要呼叫更新命令更新 likeNum:

  • hincrby sport:ranking:why:20210227:jay likeNum 500

執行完成之後點贊數就會變成 1020:

這樣,排行榜上的所有欄位我們都能獲取到了,微信排行榜就說完了。

呃......

怎麼感覺還是 API 教學呢?

不得勁,換個其他的。

最近七天排行榜怎麼弄?

前面我們說的都是每日排行榜。

假設面試官要求我們提供一個最近七天、上一週、上一月、上個季度、這一年排行榜啥的,又該怎麼搞呢?

其實這還是在考察你對於 Redis 有序集合 API 的掌握程度。

也就是這個 API:

  • zinterstore/zunionstore destination numkeys key [key ...] [weights weight [weight ...]] [aggregate sum|min|max] 獲取交集/並集

這個 API 看起來有點複雜,不要怕,一個個的講:

  • zinterstore/zunionstore其實就是交集/並集
  • destination 將交集/並集的結果儲存到這個鍵中
  • numkeys 需要做交集/並集的集合的個數
  • key [key ...] 具體參與交集/並集的集合
  • weights weight [weight ...] 每個參與計算的集合的權重。在做交集/並集計算時,每個集合中的 member 會把自己的 score 乘以這個權重,預設為 1。
  • aggregate sum|min|max 對於各個集合中的相同元素是 sum(求和)、min(取最小值)還是max(取最大值),預設為 sum。

拿最近七天舉例,我們隨便搞點資料進來,你可以直接粘過去玩:

  • zadd sport:ranking:why:20210222 43243 why 2341 mx 8764 les 42321 skr
  • zadd sport:ranking:why:20210223 57632 why 24354 mx 4231 les 43512 skr 5341 jay
  • zadd sport:ranking:why:20210224 10026 why 12344 mx 54312 les 34531 skr 43512 jay
  • zadd sport:ranking:why:20210225 54312 why 32451 mx 23412 les 21341 skr 56321 jay
  • zadd sport:ranking:why:20210226 3212 why 63421 mx 53652 les 45621 skr 5723 jay
  • zadd sport:ranking:why:20210227 5462 why 10158 mx 30169 les 48858 skr 66079 jay
  • zadd sport:ranking:why:20210228 43553 why 4451 mx 7431 les 9563 skr 8232 jay

可以看到我們一共有 7 天的資料:

而且需要注意的是 20210222 這一天是沒有 jay 的資料的。

現在我們要求出最近 7 天的排行榜,就用下面這行命令,命令有點複雜,但是對著命令格式看,還是很清晰的:

相關推薦

面試設計一個排行榜

這是why哥的第89篇原創文章 前兩天,有一個讀者給我發了一張圖片。 我問:發什麼腎麼事了? 於是有了這樣的對話: 他發的圖,就是微信運動步數排行榜的截圖: 其實扯了這麼多,這就是個常見的面試場景題:如何設計一個排行榜? 這個題吧,其實就是考你面試準備範圍的廣度,見過就會答,沒見過...就難說了。 當

阿里面試實現一個執行緒安全並且可以設定過期時間的LRU快取

目錄1. LRU 快取介紹2. ConcurrentLinkedQueue簡單介紹3. ReadWriteLock簡單介紹4.ScheduledExecutorService 簡單介紹5. 徒手擼一個執行緒安全的 LRU 快取5.1. 實現方法5.2. 原理5.3. put方法具體流程分析5.4. 原始碼6.

面試知不知道非同步程式設計的Future

荒腔走板 大家好,我是 why,歡迎來到我連續周更優質原創文章的第 60 篇。 老規矩,先來一個簡短的荒腔走板,給冰冷的技術文注入一絲色彩。 上面這圖是我五年前,在學校宿舍拍的。 前幾天由於有點事情,打開了多年沒有開啟的 QQ。然後突然推送了一個“那年今日”傳送的動態。 這張圖片就是那個動態裡面的。 20

太刺激面試手寫跳錶用兩種實現方式吊打TA!

# 前言 > 本文收錄於專輯:[http://dwz.win/HjK](http://dwz.win/HjK),點選解鎖更多資料結構與演算法的知識。 你好,我是彤哥。 上一節,我們一起學習了關於跳錶的理論知識,相信通過上一節的學習,你一定可以給面試官完完整整地講清楚跳錶的來龍去脈,甚至能夠邊講邊畫

Java程式設計師:因為不太瞭解JVM面試先回去等通知...

群裡一小夥伴抱著僥倖心裡,投了阿里簡歷,本來不抱什麼希望,意外中收到了螞蟻的面試通知,對這哥們來說,簡直“受寵若驚”。不過,他心態

騰訊面試Java中boolean型別佔用多少個位元組?一個面試回家等通知

本文首發於微信公眾號:程式設計師喬戈裡 什麼是boolean型別,根據官方文件的描述: boolean: The boolean data type has only two possible values: true and false. Use this data type

因為說:volatile 是輕量級的 synchronized面試回去等通知!

# 因為我說:volatile 是輕量級的 synchronized,面試官讓我回去等通知! > volatile 是併發程式設計的重要組成部分,也是面試常被問到的問題之一。不要向小強那樣,因為一句:volatile 是輕量級的 synchronized,而與期望已久的大廠失之交臂。 volatile

阿里P7崗位面試面試:為什麼HashMap底層樹化標準的元素個數是8

前言 先宣告一下,本文有點標題黨了,像我這樣的菜雞何德何能去面試阿里的P7崗啊,不過,這確實是阿里p7級崗位的面試題,當然,參加面試的人不是我,而是我部門的一個大佬。他把自己的面試經驗分享給了我,也讓我間接體會下阿里級別的面試難度,這樣算起來,我也勉強算是經歷面試過阿里P7的崗位的人吧,頓時感覺信心暴漲。

因為不知道Java的CopyOnWriteArrayList面試回去等通知

先看再點贊,給自己一點思考的時間,微信搜尋【沉默王二】關注這個靠才華苟且的程式設計師。本文 GitHub github.com/itwanger 已收錄,裡面還有一線大廠整理的面試題,以及我的系列文章。 hello,同學們,大家好,我是沉默王二,在我為數不多的面試經歷中,有一位姓馬的面試官令我印象深刻

【MySQL】這樣分析MySQL中的事務面試刮目相看!!

## 寫在前面 > 相信大部分小夥伴在面試過程中,只會針對面試官提出的表面問題來進行回答。其實不然,面試官問的每一個問題都是經過深思熟慮的,面試的時間相對來說也是短暫的,面試官不可能在很短的時間內就對你非常瞭解,他想通過幾個問題來考察你所掌握的知識的深度和廣度,如果你只是回答面試官表面問你的問題,向擠

「每日一題」有人上次在dy面試面試:vue資料繫結的實現原理你說該如何回答?

關注「鬆寶寫程式碼」,精選好文,每日一題 ​時間永遠是自己的 每分每秒也都是為自己的將來鋪墊和增值 >作者:saucxs | songEagle >來源:原創 ## 一、前言 文章首發在「鬆寶寫程式碼」 2020.12.23 日剛立的 flag,每日一題,題目型別不限制,可以是:演算法題,面試

前幾天去上海寶山面試(tianyi科技)面試一個問題

問題:自己是否可以定義一個集合使其支援增強for迴圈,可以請寫出,不可以請說明理由。 當時不知道,哎,太弱了! 答案: 可以,增強for迴圈不過是Java一個語法糖 還有其他語法糖, 比如泛型中的型別擦除,自動拆箱與裝箱,邊長引數,增強for迴圈,內部類與列舉類 增強for迴圈,只要你的

某程序員趣聞:面試曾經的面試把他曾經問的又問他一遍

相聲 docker 招生信息 流轉 零基礎入門 inf ali image 開會 想必最近有不少互聯網同行都在面試,大家進行的怎麽樣呢? 近日,騰訊某程序員在互聯網社區分享了自己作為面試官的趣聞:今天面試了個百度來的,他不記得我了,我前年在百度二面的面試官就是他&hell

面試使用Dubbo有沒有遇到一些坑?

開發十年,就只剩下這套架構體系了! >>>   

面試Redis分散式鎖如何續期?懵

開發十年,就只剩下這套架構體系了! >>>   

面試“Java中的鎖有哪些?以及區別”

讀寫鎖 queue get 吞吐量 參考 示例 事情 自動 高並發 在讀很多並發文章中,會提及各種各樣鎖如公平鎖,樂觀鎖等等,這篇文章介紹各種鎖的分類。介紹的內容如下: 公平鎖/非公平鎖 可重入鎖 獨享鎖/共享鎖 互斥鎖/讀寫鎖 樂觀鎖/悲觀鎖 分段鎖 偏

以為對Mysql索引很瞭解直到遇到阿里的面試

GitHub 4.8k Star 的Java工程師成神之路 ,不來了解一下嗎? GitHub 4.8k Star 的Java工程師成神之路 ,真的不來了解一下嗎? GitHub 4.8k Star 的Java工程師成神之路 ,真的確定不來了解一下嗎? 本文來自一位不願意透露姓名的粉絲投稿 相信很多人對於My

阿里面試講講Unicode3秒說沒面試說你可真菜

本文首發於微信公眾號:程式設計師喬戈裡 喬哥:首先說說什麼是Unicode、碼點吧~要想搞懂,這些概念必須清楚 什麼是Unicode? 下圖來自http://www.unicode.org/standard/WhatIsUnicode.html中的截圖 Unicode編碼定義了這個世界上幾

【高併發】面試如何使用Nginx實現限流如此回答輕鬆拿到Offer!

## 寫在前面 > 最近,有不少讀者說看了我的文章後,學到了很多知識,其實我本人聽到後是非常開心的,自己寫的東西能夠為大家帶來幫助,確實是一件值得高興的事情。最近,也有不少小夥伴,看了我的文章後,順利拿到了大廠Offer,也有不少小夥伴一直在刷我的文章,提升自己的內功,最終成為自己公司的核心業務開發人

面試redis資料型別回答8種

> **面試官**:小明呀,redis 有幾種資料結構呀? > > **小明**:8 種 > > **面試官**:那你說一下分別是什麼? > > **小明**:raw,int,ht,zipmap,linkedlist,ziplist,intset,skiplist,embstr > > **面試官**:額,你