redis-zset分页查询去重组件

redis-zset分页查询去重组件

背景:

在工作中大量用到redis做数据存储,特别是zset结构非常适合做用户排行榜等包含权重的列表。假设一个场景:

  1. 实现一个用户排序列表
  2. 使用了zset结构存储这个列表,member元素为用户id,score分值为权重
  3. 分页查询:假设查询到第2页时,第1页数据发生变化,或者第1页有一条数据被顶到第二页,就可能产生重复数据
  4. 要解决分页数据重复的问题,一般有两种方案:
    • 缓存快照,每个线程查询第一页时,缓存到快照,设定失效时间。保证这一次分页查询的生命周期内数据不会重复
    • 已读缓存,每个线程查询第一页时,生成去重缓存(设定失效时间,下次请求第一页时也会失效),每返回一页数据时先根据已读缓存过滤一遍,再将新一页数据返回并添加到已读缓存。
  5. 下面针对已读缓存的方案,实现组件

引入依赖:

<!-- https://mvnrepository.com/artifact/org.springframework.data/spring-data-redis -->
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>2.2.4.RELEASE</version>
</dependency>

实现代码:

接口:

package com.cache.service;

import org.springframework.data.redis.core.ZSetOperations;

import java.util.List;
import java.util.Set;

/**
 * redis排名列表操作接口
 */
public interface RedisZSetRankingService {
    
    

    /**
     * 查询有序列表(过滤重复数据)
     * @param key zset key
     * @param id 唯一id(用户id、当前线程id)
     * @param lastScore 上一页的最后一个score值
     * @param pageSize 查询页大小
     * @param sort 排序方式 1:升序;-1:降序
     * @param blackSet 需要过滤的黑名单列表
     * @return 返回结果
     */
    List<ZSetOperations.TypedTuple<Object>> findRankList(String key, Object id, Double lastScore, long pageSize, long sort, Set<Long> blackSet);

    /**
     * 查询有序列表根据上一页最后一个元素(过滤重复数据)
     * @param key zset key
     * @param id 唯一id(用户id、当前线程id)
     * @param lastMember 上一页最后第一个元素
     * @param pageSize 查询页大小
     * @param sort 排序方式 1:升序;-1:降序
     * @param blackSet 需要过滤的黑名单列表
     * @return 返回结果
     */
    List<ZSetOperations.TypedTuple<Object>> findRankListByLastMember(String key, Object id, Object lastMember, long pageSize, long sort, Set<Long> blackSet);
}

实现类:

package com.cache.service.impl;

import com.vdpub.cache.config.RedisZSetRankConfig;
import com.vdpub.cache.service.RedisZSetRankingService;
import com.vdpub.common.util.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.util.Assert;

import java.text.MessageFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;

public class RedisZSetRankingServiceImpl implements RedisZSetRankingService {
    
    

    private final Logger logger = LoggerFactory.getLogger(getClass());

    private final RedisTemplate<String, Object> redisTemplate;

    private final RedisZSetRankConfig redisZSetRankConfig;

    private final Map<String, RedisZSetRankConfig> configMap;
    /**
     * 去重key
     */
    private static final String DEDUPLICATION_KEY = ":deduplication:{0}";


    private static String getDeduplicationKey(String key, Object id) {
    
    
        return MessageFormat.format(key + DEDUPLICATION_KEY, String.valueOf(id));
    }


    private RedisZSetRankConfig getConfig(String key) {
    
    
        return Optional.ofNullable(configMap.get(key.replaceAll(":", ""))).orElse(redisZSetRankConfig);
    }

    public RedisZSetRankingServiceImpl(RedisTemplate<String, Object> redisTemplate, RedisZSetRankConfig redisZSetRankConfig) {
    
    
        this.redisTemplate = redisTemplate;
        this.redisZSetRankConfig = redisZSetRankConfig;
        this.configMap = redisZSetRankConfig.getMap() != null ? redisZSetRankConfig.getMap() : new HashMap<>();
    }

    @Override
    public List<ZSetOperations.TypedTuple<Object>> findRankList(String key, Object id, Double lastScore, long pageSize, long sort, Set<Long> blackSet) {
    
    
        Assert.notNull(key, "param key is null");
        Assert.notNull(id, "param id is null");
        // 获取去重集合key
        String deduplicationKey = getDeduplicationKey(key, id);
        // 查询第一页删除去重集合
        if (lastScore == null) {
    
    
            redisTemplate.delete(deduplicationKey);
        }
        RedisZSetRankConfig config = getConfig(key);
        int recursion = 0;
        long offset = 0;
        List<ZSetOperations.TypedTuple<Object>> result = new ArrayList<>();
        while (result.size() < pageSize && recursion++ <= config.getRecursion()) {
    
    
            Set<ZSetOperations.TypedTuple<Object>> objects;
            if (sort < 0) {
    
    
                objects = redisTemplate.opsForZSet().reverseRangeByScoreWithScores(key, Long.MIN_VALUE, lastScore != null ? lastScore : Long.MAX_VALUE, offset, pageSize);
            } else {
    
    
                objects = redisTemplate.opsForZSet().rangeByScoreWithScores(key, lastScore != null ? lastScore : Long.MIN_VALUE, Long.MAX_VALUE, offset, pageSize);
            }
            if (CollectionUtils.isEmpty(objects)) {
    
    
                break;
            }
            // 第一页请求不需要过滤重复
            // 过滤完将已读元素放到去重集合
            for (ZSetOperations.TypedTuple<Object> o : objects) {
    
    
                if(o.getValue() == null){
    
    
                    continue;
                }
                Long member = ((Number) o.getValue()).longValue();
                if((lastScore == null || Boolean.FALSE.equals(redisTemplate.opsForSet().isMember(deduplicationKey, member))) && !blackSet.contains(member)){
    
    
                    redisTemplate.opsForSet().add(deduplicationKey, o.getValue());
                    result.add(o);
                    if (result.size() >= pageSize) {
    
    
                        break;
                    }
                }
            }
            Optional<ZSetOperations.TypedTuple<Object>> first = objects.stream().skip(objects.size() - 1).findFirst();
            if(!first.isPresent()){
    
    
                break;
            }
            offset = lastScore != null && lastScore.equals(first.get().getScore()) ? objects.size() : 0;
            lastScore = first.get().getScore();
            if (recursion >= config.getRecursion()) {
    
    
                logger.info("findRevRankList recursion over-limit key={}, id={}", key, id);
            }
        }
        redisTemplate.expire(deduplicationKey, config.getDeduplicationExpire(), TimeUnit.SECONDS);
        return result;
    }

    @Override
    public List<ZSetOperations.TypedTuple<Object>> findRankListByLastMember(String key, Object id, Object lastMember, long pageSize, long sort, Set<Long> blackSet) {
    
    
        Double lastScore = null;
        if (lastMember != null) {
    
    
            lastScore = redisTemplate.opsForZSet().score(key, lastMember);
            if (lastScore == null) {
    
    
                logger.warn("findRevRankListByLastMember lastMemberNotFound Key={}, lastMember={}", key, lastMember);
                return new ArrayList<>();
            }
        }
        return this.findRankList(key, id, lastScore, pageSize, sort, blackSet);
    }

}

配置类:

package com.cache.config;

import java.util.Map;


public class RedisZSetRankConfig {
    
    

    /**
     * 默认配置:循环次数(特殊情况下查到一页数据全部为重复数据后,自动查询下一页的次数)
     */
    private int recursion = 5;

    /**
     * 默认配置:去重缓存过期时间 单位:s
     */
    private long deduplicationExpire = 7200;

    /**
     * 特殊配置:每个zset-key都可以单独配置,如果不配,使用默认
     */
    private Map<String, RedisZSetRankConfig> map;

    public long getDeduplicationExpire() {
    
    
        return deduplicationExpire;
    }

    public void setDeduplicationExpire(long deduplicationExpire) {
    
    
        this.deduplicationExpire = deduplicationExpire;
    }

    public int getRecursion() {
    
    
        return recursion;
    }

    public void setRecursion(int recursion) {
    
    
        this.recursion = recursion;
    }

    public Map<String, RedisZSetRankConfig> getMap() {
    
    
        return map;
    }

    public void setMap(Map<String, RedisZSetRankConfig> map) {
    
    
        this.map = map;
    }
}

spring加载配置:

  	/**
  		将组件加载到spring容器
  	*/
	@Bean("redisZSetRankConfig")
    @ConfigurationProperties(prefix = "ranking.zset")
    public RedisZSetRankConfig redisZSetRankConfig() {
    
    
        return new RedisZSetRankConfig();
    }

    @Bean("redisZSetRankingService")
    public RedisZSetRankingService RedisZSetRankingService(@Qualifier("redisTemplate") RedisTemplate<String, Object> redisTemplate, RedisZSetRankConfig redisZSetRankConfig){
    
    
        return new RedisZSetRankingServiceImpl(redisTemplate, redisZSetRankConfig);
    }

yaml配置:

ranking:
  zset:
    recursion: 5
    deduplicationExpire: 7200
    map:
      zset_key1:
        recursion: 10
        deduplicationExpire: 1000
      zset_key2:
        recursion: 15
        deduplicationExpire: 2000
		

猜你喜欢

转载自blog.csdn.net/Arhhhhhhh/article/details/132457929