天天看點

redis在排行榜中的使用總結前言

<a href="https://redis.io">redis官網</a> redis 是一個開源(bsd許可)的,記憶體中的資料結構存儲系統,它可以用作資料庫、緩存和消息中間件。它支援多種類型的資料結構,如 字元串(strings), 散列(hashes), 清單(lists), 集合(sets), 有序集合(sorted sets) 與範圍查詢, bitmaps, hyperloglogs 和 地理空間(geospatial) 索引半徑查詢。 redis 内置了 複制(replication),lua腳本(lua scripting), lru驅動事件(lru eviction),事務(transactions) 和不同級别的 磁盤持久化(persistence), 并通過 redis哨兵(sentinel)和自動 分區(cluster)提供高可用性(high availability)

正如上面的介紹,redis支援很多種類型的資料結構,适用于多種場景。目前應用最為廣泛的場景主要分為以下幾類:

會話緩存(session cache)

隊列

排行榜/清單

計數器

釋出/訂閱

本篇,我主要是從排行榜的業務場景下,進行的一些個人總結。

排行榜業務變化多樣,從不同的角度思考,是不同的排行榜需求,但總結起來,主要分為以下幾類:

從排行榜的實效性上劃分,主要分為:

實時榜:基于目前一段時間内資料的實時更新,進行排行。例如:目前一小時内遊戲熱度實時榜,目前一小時内明星送花實時榜等

曆史榜:基于曆史一段周期内的資料,進行排行。例如:日榜(今天看昨天的),周榜(上一周的),月榜(上個月的),年榜(上一年的)

從需要排行的資料類型上劃分,主要分為:

單類型資料排行榜:是指需要排行的主體不需要區分類型,例如,所有使用者積分排行,所有公貢獻值排行,所有遊戲熱度排行等

多類型(複合類型)資料排行榜:是指需要排行的主體在排行中要求有類型上的區分,例如:競技類遊戲熱度排行、體育類遊戲熱度排行、moba類遊戲操作性排行、角色/回合/卡牌三類遊戲熱度排行等

從榜單的最終展示唯度上劃分,主要分為:

單唯度:是指選擇展示的排行榜就是基于一個唯度下的排行,例如前面提到的moba類遊戲操作性排行榜,就僅展示所有moba類遊戲按操作性的評分排行

多唯度:是指選擇展示的排行榜還有多種唯度供使用者選擇,仍然以前面的moba類遊戲為例,唯度除了操作性,還有音效評分排行,難易度評分排行,畫面評分排行等。

從需要展示的資料量上劃分,主要分為:

topn資料:隻要求展示topn條排行紀錄,例如:最火moba遊戲top20

全量資料:要求展示所有資料的排行,例如:所有使用者的積分排行

在以上幾種榜單中,往往還會有需要加入,目前使用者自身的一些資料在排行榜中的位置。

選擇合适的redis資料結構,可以快速的實作排行榜要求。

首選sorted set資料結構:

redis sorted sets — sorted sets are a data type which is similar to a mix between a set and a hash. like sets, sorted sets are composed of unique, non-repeating string elements, so in some sense a sorted set is a set as well.

however while elements inside sets are not ordered, every element in a sorted set is associated with a floating point value, called the score (this is why the type is also similar to a hash, since every element is mapped to a value).

moreover, elements in a sorted sets are taken in order (so they are not ordered on request, order is a peculiarity of the data structure used to represent sorted sets). they are ordered according to the following rule:

if a and b are two elements with a different score, then a &gt; b if a.score is &gt; b.score.

if a and b have exactly the same score, then a &gt; b if the a string is lexicographically greater than the b string. a and b strings can’t be equal since sorted sets only have unique elements.

在sorted-set中添加、删除或更新一個成員都是非常快速的操作,其時間複雜度為集合中成員數量的對數。由于sorted-sets中的成員在集合中的位置是有序的,是以,即便是通路位于集合中部的成員也仍然是非常高效的。

sorted-set底層的實作是跳表(skiplist),插入和删除的效率都很高.

redis在排行榜中的使用總結前言

在redis的源碼中,找到zset的定義如下(server.h):

看了上面的插入節點動畫示範,再來看插入節點對應的方法源碼(t_zset.c),了解起來就容易多了:

(p.s:skip list 看似一個簡單的連結清單分層設計,卻可以取得很好的效果,大道至簡!)

了解redis中的sorted set為何可以做到快速的排序之後,接下來就是我們在實際的業務中,如何來利用它的這個特性了。

在我們的業務排行榜中,在決定使用redis來做排序之前,我們往往還有其他方案可供選擇,比如:采用資料庫存儲資料,建構好相應的索引,通過sql查詢來直接排行,也是可以做到的。個人認為,如果資料量不大,而且排行榜類型也不多,完全可以用sql來解決,畢竟引入redis,也還需要考慮很多運維場景。

如果我們選擇使用redis,那如何定義好業務的key,member,和scores,就很重要了。

根據前面所總結的排行榜業務的分類不同,設計時思考的角度也都不同,但基本上我會按照以下步驟:

規範key的命名:一般我們采用這種格式,服務代号:業務代号:排行主體:榜單分類:%s_版本号。在這裡的變量%s,也會根據不同的排行榜進行定義,往往是排序對象的父級,例如:所有遊戲使用者日積分排行榜,排行展示的對象是使用者清單,在該榜單中使用者歸屬至某一天,那對應的%s就可以定義為yyyymmdd格式的具體日期,這樣就可以很友善我們進行key的組裝和資料查詢;類似,某款遊戲下的使用者累積充值排行榜,排行展示的對象是使用者清單,在該榜單中的使用者歸屬于遊戲,那對應的%s可以是遊戲id,再比如,競技類遊戲熱度排行榜,排行展示的是遊戲清單,遊戲都歸屬在這個競技類下面,那這裡的%s可以是該遊戲類别。

确定memeber:将需要排序的對象,也就是最終要展示在榜單清單中的對象定義為member。就像上面提到所有遊戲使用者日積分排行榜中對應的member是使用者(uid)、競技類遊戲熱度排行榜中對應的member是遊戲(gameid)

确定scores:大部份的情況下,scores的值就是最終用來比較的值,例如上面提到的積分值、充值值、熱度值,還有的可能是時間值,也可能是一個經過複合計算的權重值(小技巧),目的就是為了提供給sorted set進行比較排序

有效期:根據上面第一節中榜單的時效性分類,對應不同的緩存時間

明星收花自然周實時榜(遊戲使用者給喜歡的遊戲代言明星送花,使用者即可檢視目前所有明星的本周排行榜,一周有效,獲得花越多明星的排名越前,分頁展示),也可以檢視具體一個明星下,送花的使用者的排行榜,當天有效。根據前面的分類,明星本周排行,需要最終展示的清單是明星,那member必定是明星(starid),而歸屬的實效性上是自然周,是以我們key值用自然周來進行組合,于是我們做下面這種設計:

具體明星下的送花使用者排行榜(使用者送的越多,排名越前,分頁展示),這種場景,這時要求展示的清單是普通使用者,那member我們選擇普通使用者(uid),而其歸屬是在一個具體的明星下,是以我們key值用starid來進行組合,于是我們也可以做這種設計:

使用者送花自然周實時榜、使用者送花自然月實時榜也類似。

是不是覺其實挺簡單,隻要我們能清楚的将需要展示的排行榜,确定member,界定key,score值的量化,那這種排行榜實作起來是不是比資料庫來做就容易多了。

上面舉的列子中,score值都是屬于比較确定的,在業務動作發生時,通過不斷的累積,能有一個具體的值。還有一些業務場景,這種score值并不是由簡單的一個确定了的數字,這個時候往往需要我們自己定義一種計算公式,通過一些簡單的運算得到一個“權值”;其次,上面的member也都是可以直接确定的對象id,有的些業務排行榜中,我們可能member也有可能遇到組合的方式。

不管member是唯一的id,還是組合而成,我們都需要注意以下兩點:

member在sorted set資料結構中,必須是唯一的

相同score的情況下,是以member的字典順序排序的

member組合的場景,例如:如果我們需要一種實時排行榜,總是排行目前3小時内的某個明星下的所有送花使用者排行清單(如,從14:01--17:01,從14:02--17:02...........),那我們可以這樣設計:

還有一些排行榜中,我們有時候甚至需要将多個key進行重新組合再排序,這種排行有一個顯著的特殊,那就是排行榜中展示的主體對象是有較多類型的,也是說對應我們上面排行榜分類中所提到的,從業務資料類型上劃分中的多類型那一類。舉個例子:遊戲分為競技類遊戲、動作類遊戲、賽車類遊戲,如果要求每一種類型的遊戲都有熱度排行榜,我們可以很輕松的設計出key:

如果後面又想将這三種類型的遊戲組合到一起進行排序,那又該怎麼辦?

這就需要通過zunionstore來實作了,通過zunionstore來得到一個并集,并最終排序。但這裡需要注意的是需要組成并集的各個集合的key必須是對應到redis叢集中的同一個slot上,否則将會出現一個異常:crossslot keys in request don't hash to the same slot。是以redis提供了一種特定的标簽{},這個{}内的字元串才參與計算hash slot.列如:{user}:aaa與{user}:bbb 這兩個集合可以確定在同一個slot上,可以使用zunionstore求它們的并集。是以,在處理這種需要合并redis曆史資料時,如果是在redis叢集環境下,需要特别注意。

類似的,我們有時候可以通過日榜組合三日榜,七日榜,組合月榜,組合年榜等等

是不是看起來排行榜也挺容易實作的,趕緊試試吧

在基于redis的整個排行榜的設計過程中,我們還需要考慮的

排行榜key的數量:確定key的增長數量是可控的,可設定過期時間的,就設定明顯的過期時間

占用空間評估:redis中排行榜資料記憶體占用情況進行評估

<a href="https://en.wikipedia.org/wiki/skip_list">https://en.wikipedia.org/wiki/skip_list</a>

<a href="https://redis.io/">https://redis.io/</a>

<a href="http://redis.cn/">http://redis.cn/</a>