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緩存出現問題導致系統癱瘓。