Redis的ZSet排行榜功能實現
1. 功能需求
類似給用戶n張圖片, 用戶左滑不喜歡右滑喜歡。所以每個用戶就會有一些喜歡的圖片集合和不喜歡的圖片集合。現在我們要做一個將按照一個算法將喜歡的排到前面。算法 ctr = (喜歡數+20)/ (喜歡數+不喜歡數+20),所有的內容按照這個算法的結果進行排行榜排序。
2. Redis sorts sets簡介
Sorted-Sets和Sets類型極為相似,它們都是字符串的集合,都不允許重復的成員出現在一個Set中。它們之間的主要差別是Sorted-Sets中的每一個成員都會有一個分數(score)與之關聯,Redis正是通過分數來為集合中的成員進行從小到大的排序。然而需要額外指出的是,盡管Sorted-Sets中的成員必須是唯一的,但是分數(score)卻是可以重復的。
Sorted Sets是通過Skip List(跳躍表)和hash Table(哈希表)的雙端口數據結構實現的,因此每次添加元素時,Redis都會執行O(log(N))操作。所以當我們要求排序的時候,Redis根本不需要做任何工作了,早已經全部排好序了。元素的分數可以隨時更新。
3. 代碼實現
本文主要通過redisTemplate來操作redis,當然也可以使用redis-client,看個人喜好。
首先寫兩個要用到的兩個方法, 一個批量插入數據,一個獲取排行榜Top n。
/**
* @Description: 批量添加zset數據
* @author mazhq
*/
public Long setBatchZSet(String key, Set<ZSetOperations.TypedTuple<Object>> typedTuples) {
try {
return redisTemplate.opsForZSet().add(key, typedTuples);
} catch (Exception e) {
logger.error("redis setZSet failed, key = " + key + "| error:" + e.getMessage(), e);
return 0L;
}
}
/**
* @Description: 獲取排行前面的數據
* @author mazhq
*/
public List<Object> getTopRankZSet(String key, int topCount) {
try {
Set<Object> range = redisTemplate.opsForZSet().reverseRange(key, 0, topCount);
return Arrays.asList(range.toArray());
} catch (Exception e) {
logger.error("redis getTopRankZSet failed, key = " + key + "| error:" + e.getMessage(), e);
return new ArrayList<>();
}
}
插入排行榜數據
/**
* @Description: 批量添加圖片排行榜數據
* @author mazhq
*/
public void batchAddImageData(){
//獲取喜歡和不喜歡的map數據 key是圖片ID
Map<String, Integer> map = userBehaviorRecordManager.getQuickImageStatistic();
//獲取所有圖片列表
List<ImageConfigBean> imageConfigBeanList = quickImageConfigManager.getRealAllList();
Set<ZSetOperations.TypedTuple<Object>> tuples = new HashSet<>();
for (ImageConfigBean imageConfigResp : imageConfigBeanList) {
String likeKey = imageConfigResp.getGuid() + QuickConstant.LIKE;
String unLikeKey = imageConfigResp.getGuid() + QuickConstant.UNLIKE;
//ctr算法 以1000為統計精確維度 即精確到小數點后三位
double ctr = 1000d;
if (map.containsKey(likeKey) && map.containsKey(unLikeKey)) {
double total = (map.get(likeKey)).doubleValue() + map.get(unLikeKey).doubleValue() + 20d;
double ctrStatistic = (map.get(likeKey).doubleValue() + 20d) / total;
ctr = (double) Math.round(ctrStatistic * 1000);
}else if(!map.containsKey(likeKey) && map.containsKey(unLikeKey)){
double total = map.get(unLikeKey).doubleValue() + 20d;
double ctrStatistic = 20d / total;
ctr = (double) Math.round(ctrStatistic * 1000);
}
DefaultTypedTuple<Object> tuple = new DefaultTypedTuple<>(imageConfigResp.getGuid() + "", ctr);
tuples.add(tuple);
}
redisClient.setBatchZSet(RedisKeysManager.getMiniProgramSlideRankingKey(), tuples);
}
獲取Top50排行榜數
/**
* @Description: 獲取排行榜top50條記錄
* @author mazhq
*/
@RequestMapping("/getTop50")
public String getTop50() {
List<Object> stringList = redisClient.getTopRankWithScoresZSet(RedisKeysManager.getMiniProgramSlideRankingKey(), 50);
return JSONObject.toJSONString(stringList);
}
其它集合操作方法
//單個增加集合內容
public boolean setSortedSet(String key, double score, Object value) {
try {
return redisTemplate.opsForZSet().add(key, value, score);
} catch (Exception e) {
logger.error("redis setSortedSet failed, key = " + key + "| error:" + e.getMessage(), e);
return false;
}
}
//單個增加分數
public double incrementScore(String key, double score, Object value) {
try {
return redisTemplate.opsForZSet().incrementScore(key, value, score);
} catch (Exception e) {
logger.error("redis incrementScore failed, key = " + key + "| error:" + e.getMessage(), e);
return 0.0;
}
}
//單個刪除
public boolean delSortedSet(String key, Object... values) {
try {
long count = redisTemplate.opsForZSet().remove(key, values);
return count > 0;
} catch (Exception e) {
logger.error("redis delSortedSet failed, key = " + key + "| error:" + e.getMessage(), e);
return false;
}
}
4. 總結
新增or更新
//單個新增or更新 Boolean add(K key, V value, double score); //批量新增or更新 Long add(K key, Set<TypedTuple<V>> tuples); //使用加法操作分數 Double incrementScore(K key, V value, double delta);
刪除
//通過key/value刪除 Long remove(K key, Object... values); //通過排名區間刪除 Long removeRange(K key, long start, long end); //通過分數區間刪除 Long removeRangeByScore(K key, double min, double max);
查尋
//通過排名區間獲取列表值集合 Set<V> range(K key, long start, long end); //通過排名區間獲取列表值和分數集合 Set<TypedTuple<V>> rangeWithScores(K key, long start, long end); //通過分數區間獲取列表值集合 Set<V> rangeByScore(K key, double min, double max); //通過分數區間獲取列表值和分數集合 Set<TypedTuple<V>> rangeByScoreWithScores(K key, double min, double max); //通過Range對象刪選再獲取集合排行 Set<V> rangeByLex(K key, Range range); //通過Range對象刪選再獲取limit數量的集合排行 Set<V> rangeByLex(K key, Range range, Limit limit); //獲取個人排行 Long rank(K key, Object o); //獲取個人分數 Double score(K key, Object o);
統計
//統計分數區間的人數 Long count(K key, double min, double max); //統計集合基數 Long zCard(K key);
基本整理了排行榜用到的所有方法,排行榜有這一篇文章夠用了。同時大家注意當redis緩存被清空,如何重新計算排行榜相關數據,或者安排定時排行榜數據定時落地邏輯。
避免redis緩存出現問題導致系統癱瘓。
