redis zset做排行榜


直播運營活動中經常會有這樣的需求,根據用戶送禮情況做排名。這個排行榜具有以下特點:

  1. 用戶每次請求會返回用戶的排名
  2. 送禮金額越多粉絲排名越靠前
  3. 相同金額送禮越早越靠前
  4. 排行榜會隨着粉絲送禮變化而不斷變化
  • 排行榜的實現方式
表結構
CREATE TABLE `user` ( `id` int(10) NOT NULL COMMENT '編號', `uid` varchar(32) NOT NULL COMMENT '用戶', `coin` int(10) NOT NULL COMMENT '用戶送出金額', `create_time` datetime NOT NULL COMMENT '創建時間', `update_time` datetime NOT NULL COMMENT '更新時間', PRIMARY KEY (`id`), UNIQUE KEY `uid` (`uid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用戶表'; 
1. sql查詢
EXPLAIN SELECT * FROM ( SELECT @rank := @rank + 1 AS rank, s.uid AS uid, s.coin AS coin FROM `user` s, (SELECT @rank := 0) r ORDER BY coin DESC, create_time ) q WHERE q.uid = 'xiaoming'; 

根據 select @rank 與 user表結合起來作為一張有排名的新表,然后再從中找出某個用戶的排名。這種方法的優點是簡單,每次用戶來請求,只要用這個SQL查一下即可;缺點是這個算是比較復雜的SQL,查起來太慢,每次都要全表查詢,試了幾次都要0.5s左右。用explain分析如下:


 
explain
2. user中添加rank字段

在user中添加rank字段,寫個計划任務每隔2分鍾全表掃描,然后更新rank名次字段。這個方法最殘暴,但是也是最不可取的。首先會產生延遲,因為2分鍾才更新一次名次,其次,每次都要更新全部數據,給數據庫很大的壓力,最后,計划任務更新數據的時候,用戶送禮也在更新數據,稍微不注意就會出現臟讀的情況。

3. 用Redis的zset數據結構
  • ZSet實現排行榜

zset的相關api (PipelineCluster/Jedis)
  1. 插入或者更新數據
    Long zadd(final String key, final double score, final String member)
    key : 排行榜的名字
    memeber : 用戶
    score : 用戶的分數
  2. 獲取用戶分數
    Double zscore(String key, final String member)
  3. 獲取用戶的排名
    Long zrevrank(final String key, final String member):(score從大到小,從0開始,所以需要加1)
    Long zrank(final String key, final String member):(score從小到大,從0開始,所以需要加1)
  4. 獲取某個范圍內的用戶排名
    Set<Tuple> zrevrangeWithScoresBytes(String key, final long start, final long end) (從大到小)
    Set<Tuple> zrangeWithScoresBytes(String key, final long start, final long end) (從小到大)
    start : 開始排名
    end : 結束排名
    Tuple :
public class Tuple implements Comparable<Tuple> { // 用戶 private byte[] element; //分數 private Double score; } 

比如我們想查1-10的排名,我們可以zrevrangeWithScoresBytes(key, 0, 9)

排行榜的實現
  1. 簡單
    簡單的排行榜就是每次用戶信息更新后,把用戶uid和用戶coin都更新到zset中,這個的好處是比較簡單,有一點不好的就是他不能實現先到先得,即先相同金額送禮越早越靠前。
  2. 較復雜(可實現先到先得)
  • 較復雜的zset和簡單的不同的是score存的不僅僅是用戶的coin,而是用戶coin 和時間戳(秒)ts的組合。為了實現先到先得的zset,可設置存進去的score = (coin * 10000000000(十次方)) + (100000000000(十一次方) - ts)
  • 表面上好像是解決了先到先得這個難題,但是實際上這樣子還不是最優解,因為存進去的score長度是有限的,據我所測,好像是18位數左右,除掉時間戳10位以后,只能存8位的coin了。這很明顯還不夠。那該怎么辦呢?
  • 我們縮短一下coin或者ts的長度不就OK了嗎?首先coin是改不了的,因為這是核心數據,所以能夠下手的就只有ts了。ts這個時間戳,其實包括了年月日分時秒,某一段相近時間內,他們的ts前幾位都是相同的。比如2018-08-01 00:00:00 的時間戳為1533052800, 2018-09-01 00:00:00 的時間戳為1535731200,相隔一個月的兩個時間,他們的前三位都是相同的,所以我們只需要取后面7位參與計算即可。取多少位取決於我們的活動要舉辦多久。我們根據開始時間和結束時間的時間戳,取出不同部分參與計算。
  • 如果ts被我們壓縮到了3位,也就是說我們的coin可以增加三位 11位的coin差不多人民幣億元起,我們歡迎砸錢超過10億的土豪讓我們的程序出現bug。
  • 以下是對coin轉score的封裝:
    /** * 將coin加密成可以存在zset的值,實際上就是 coin * 10000000 + now % 10000000 * @param coin * @return */ public static Double encrypt(Long coin){ Long value = coin * KEY + (KEY - DateUtil.getInt() % KEY); return value.doubleValue(); } /** * 將zset的值轉成long型的coin * @param value * @return */ public static Long decrypt(Double value){ Double coin = value / KEY; return coin.longValue(); }


作者:黃二的NPE
鏈接:https://www.jianshu.com/p/af99085f4b7a
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM