問題
我在做論壇的是時候遇到了如下的問題。論壇里可以有很多的主題topic,每個topic對應到很多回復reply。現在要查詢某個topic下按照replyTime升序排列的第pageNo頁的reply,每頁pageSize個reply。
reply是存放在mysql中的。以前的實現是利用mysql的limit查詢
1
2
3
4
|
select
*
from
reply
where
topicId = ?
order
by
replyTime
asc
limit (pageNo - 1) * pageSize, pageSize
|
由於現在有很多的主題的回復很多,當有人查詢第幾百甚至幾千頁的時候,mysql性能表現很不好。“select limit offset, size” 只要offset太大,傳統的關系型數據庫的性能表現都不好。
如果能夠利用帶索引的查詢條件先過濾掉一部分數據,就可以大大提高性能,比如:
1
2
3
4
5
|
select
*
from
reply
where
topicId = ?
and
replyId > lastReplyIdOfCurrentPage
order
by
replyTime
asc
limit (pageNo - currentPageNo) * pageSize, pageSize
|
lastReplyIdOfCurrentPage 是當前頁的最后一個reply的id。currentPageNo是當前頁的頁號。這里用replyId過濾條件,把前面頁的內容過濾掉,這樣減少了 offset的大小。但是當用戶需要跳轉到很遠的一個頁面的時候,offset還是會很大。比如,當前是第10頁,要跳轉到第1000頁,offset = 990 * pageSize,還是會很大,性能依舊不行。盡管目前很多產品,都不提供這樣的跳轉能力了,但是我們的產品團隊還是認為這個功能在我們的產品里面不可或 缺。
遷移到cassandra
后來我們把reply數據全部遷移到了cassandra上。cassandra的數據結構和mysql不一樣。我們創建了一個topic_reply 列簇,每一行的行號是topicId,每一列是這個topic的replyId,這樣得到類似如下結構
1:1,2,5,33,245,663,780...
2:36,78,89,94,235,345...
在cassandra中列是自然排序的,形成了一個從topic到reply的索引。查詢的時候只能查詢topicId行的列大於(或小於)replyId的size個replyId,相當於sql:
1
|
select
*
from
topic_reply
where
replyId > ? limit
size
|
, 不能夠 “limit offset, size”。這意味着如果要查詢第一千頁,而我不知道第一千頁開始的replyId是多少,我就得取出這一千頁的數據,這顯然是行不通的。所以得想辦法從靠近我要取的數據的某個replyId處開始取數據。
reids的SortedSet
無 論是mysql還是cassandra,都不能很好地解決從一個很長的序列中取出任意一段數據的問題,而造成這一問題的根源在於這些數據是存放在磁盤上 的,磁盤不適合做此類的隨機讀的操作。所以想,如果能有一個程序,管理一些很大很大的放在內存中排序數組就好了,因為對內存中的數組做下標訪問,是非常快 速的。做了一下調查正好發現,redis提供了此類的功能。
redis將數據存放到內存中,所以既便是隨機讀寫,速度都是非常快。 redis支持的SortedSet結構正好適合於做分頁查詢。SortedSet按照給定的score給member排序,允許通過下標或者score 去查詢。把同一個topic的replyId作為member,以replyId本身為score存放到SortedSet后,就可以通過下標取值了,例 如:
//存入數據
zadd tr:1 1 1
zadd tr:1 2 2
zadd tr:1 5 5
zadd tr:1 33 33
zadd tr:1 245 245
zadd tr:1 663 663
//pageSize = 3 取 第二頁,即下標 3 到5的元素
zrange tr:1 3 5
其中 tr:1 是這個SortedSet的key,”tr:”只是用來區分其它key用的前綴,1是topicId。更詳細的內容看redis官網http://redis.io
如此一來,就可以實現任意分頁查詢了,而且性能非常好。
緩存索引
redis 的數據全部存放到內存中,如果把所有topic到reply的關系都放到內存中,要耗費很多內存,而且這么多的內存實際上很多是浪費的,畢竟大部分的 topic是不活躍的。再者topic到reply的映射關系是非常重要的,所以我們需要把這種關系持久化。最后我們決定,這個映射關系,或者稱為索引還 是存放在cassandra里面,只是在需要的時候,才從cassandra里面把索引載入到redis內,然后再利用redis分頁查詢。如此一 來,redis成了一個支持分頁查詢的強大的緩存。
分片緩存
對於超長的主題,全新載入到redis一次也是相當的耗時的,我們采取分片來解決這個問題。我們把索引每4800個值分成一片,用另外一個數據結構記錄索引長度和索引從第二片開始的每片的開始值。
更新的索引的時候更新這個分片信息,記錄各分片的頭部是為了便於從cassandra載入分片。
查 詢的時候把分頁查詢轉化成某個片上某段索引的值。當分片大小大於pageSize並且能被pageSize整除時,這個轉化是很簡單的,因為分頁正好會全 部落在某一個分片中。我們之所以把分片大小設置成4800正是因為這個值能被10 15 20 25 30 40 50 60 80 100 200 等很多常用分頁大小整除。分片太大浪費內存,分片太小分片就太多。
只要算出這一頁所在的分片,然后把需要的索引段載入到redis,再利用redis的分頁查詢查出結果。這樣,只有活躍的索引分段才會被載入到redis內存中。
如果用mysql來持久化索引效果也是類似的,而且查詢更加便利能力更強。
總結
只要產品能接受,就不要使用任意分頁,任意跳轉。確實需要高速分頁查詢的時候可以使用redis的SortedSet,但是得注意內存大小和持久化問題。