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 数据
- * @param key
- * @param typedTuples
- * @return java.lang.Long
- * @exception
- * @author mazhq
- * @date 2019/7/17 16:23
- */
- 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: 获取排行前面的数据
- * @param key
- * @param topCount 数据数
- * @return java.util.List<java.lang.Object>
- * @exception
- * @author mazhq
- * @date 2019/7/17 16:24
- */
- 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: 批量添加图片排行榜数据
- * @param
- * @return void
- * @exception
- * @author mazhq
- * @date 2019/7/19 9:56
- */
- 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 条记录
- * @param
- * @return java.lang.String
- * @exception
- * @author mazhq
- * @date 2019/7/19 9:58
- */
- @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 缓存出现问题导致系统瘫痪.
来源: http://www.bubuko.com/infodetail-3127850.html