redis-zset 페이징 쿼리 중복 제거 구성 요소

redis-zset 페이징 쿼리 중복 제거 구성 요소

배경:

Redis는 업무 중 데이터 저장 용도로 널리 사용되며, 특히 zset 구조는 사용자 리더보드와 같이 가중치가 포함된 목록에 매우 적합합니다. 시나리오를 가정해 보겠습니다.

  1. 정렬된 사용자 목록 구현
  2. zset 구조는 이 목록을 저장하는 데 사용되며 멤버 요소는 사용자 ID이고 점수는 가중치입니다.
  3. 페이징 쿼리: 쿼리가 두 번째 페이지에 도달하면 첫 번째 페이지의 데이터가 변경되거나 첫 번째 페이지의 데이터 일부가 두 번째 페이지로 푸시되어 중복 데이터가 발생할 수 있다고 가정합니다.
  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容器
  	*/
	@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
		

Guess you like

Origin blog.csdn.net/Arhhhhhhh/article/details/132457929