直播運營活動中經常會有這樣的需求,根據用戶送禮情況做排名。這個排行榜具有以下特點:
- 用戶每次請求會返回用戶的排名
- 送禮金額越多粉絲排名越靠前
- 相同金額送禮越早越靠前
- 排行榜會隨着粉絲送禮變化而不斷變化
排行榜的實現方式
表結構
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)
- 插入或者更新數據
Long zadd(final String key, final double score, final String member)
key : 排行榜的名字
memeber : 用戶
score : 用戶的分數 - 獲取用戶分數
Double zscore(String key, final String member) - 獲取用戶的排名
Long zrevrank(final String key, final String member):(score從大到小,從0開始,所以需要加1)
Long zrank(final String key, final String member):(score從小到大,從0開始,所以需要加1) - 獲取某個范圍內的用戶排名
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)
排行榜的實現
- 簡單
簡單的排行榜就是每次用戶信息更新后,把用戶uid和用戶coin都更新到zset中,這個的好處是比較簡單,有一點不好的就是他不能實現先到先得,即先相同金額送禮越早越靠前。 - 較復雜(可實現先到先得)
- 較復雜的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
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。