Ranking system based on Redis

A marketing activity was conducted last month. The activity is probably divided into two teams. Users can join the team at will and improve their combat power value by constantly doing tasks. While improving their combat power value, they will also improve the combat power value of their team. In the process of continuous PK, the combat power value and ranking information of teams and specific users are constantly changing. Thanks to the love of the organization, I was entrusted with this glorious task. After comparison, I finally chose zset of redis as the technical scheme of our ranking mechanism.

However, our activities ignored a very important thing, that is, how to determine the secondary ranking under the same combat power value. Because the product realized this problem late, it was not realized in the end. First, the project has been tested and can not bear the risk of change, and second, the time is really not enough. Although the activity is over now, this problem has always been in my heart. As a programmer who draws inferences from one instance and is diligent in thinking and learning, I absolutely want to solve this problem, ha ha.

In the online Baidu scheme, it is found that most of the schemes are: combine scores and time stamps to form a score and save it to redis. The specific implementation of randomly selecting a scheme is as follows:

I would like to say: this scheme is indeed feasible, but there are defects:

1. When the combat power value is the same, how can I change it to the top ranking that reaches the combat power value at the latest?

2. If I have three levels of ranking and need to use three fields to rank, what should I do?

After learning from my experience, I came up with a set of solutions that can perfectly solve such problems: the first level combat power ranking is still placed in redis, and the second level (or even more) ranking information is stored elsewhere in another way.

Direct code:

package chen.huai.jie.springboot.ranking.service;

import chen.huai.jie.springboot.ranking.model.UserRanking;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

/**
 * @author chenhuaijie
 */
@Service
public class RankingService {

    private String key = "ranking";
    private String updateTimeKey = "updateTime";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private void putUpdateTime(String userId, Long updateTime) {
        stringRedisTemplate.opsForHash().put(updateTimeKey, userId, updateTime);
    }

    private Long getUpdateTime(String userId) {
        return (Long) stringRedisTemplate.opsForHash().get(updateTimeKey, userId);
    }

    /**
     * Add points to users
     *
     * @param userId
     * @param score
     * @return
     */
    public Double increaseUserScore(String userId, Double score) {
        putUpdateTime(userId, System.currentTimeMillis());
        return stringRedisTemplate.opsForZSet().incrementScore(key, userId, score);
    }

    /**
     * Get ranking
     * The scores are arranged in reverse order and in ascending order of time (the earlier the score is reached, the higher the ranking)
     * Note: large sorting is done in Redis, but when the scores are the same, it needs to be sorted again in memory
     *
     * @return
     */
    public List<UserRanking> getRankings() {
        Long size = stringRedisTemplate.opsForZSet().size(key);
        Set<ZSetOperations.TypedTuple<String>> typedTuples = Objects.requireNonNull(stringRedisTemplate.opsForZSet().reverseRangeWithScores(key, 0, size));
        if (CollectionUtils.isEmpty(typedTuples)) {
            return new ArrayList<>(0);
        }

        return getRankings(typedTuples, size);
    }


    /**
     * Get ranking
     * The scores are arranged in reverse order and in ascending order of time (the earlier the score is reached, the higher the ranking)
     * Note: large sorting is done in Redis, but when the scores are the same, it needs to be sorted again in memory
     *
     * @return
     */
    public List<UserRanking> getRankings(Long limit) {
        Long size = stringRedisTemplate.opsForZSet().size(key);
        if (limit > size) {
            limit = size;
        }

        Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet().reverseRangeWithScores(key, 0, limit - 1);

        // Score first and last
        Double maxScore = new ArrayList<>(typedTuples).get(0).getScore();
        Double score = new ArrayList<>(typedTuples).get(typedTuples.size() - 1).getScore();

        if (stringRedisTemplate.opsForZSet().count(key, score, score) > 1) {
            typedTuples = stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(key, score, maxScore);
        }

        return getRankings(typedTuples, limit);
    }

    private List<UserRanking> getRankings(Set<ZSetOperations.TypedTuple<String>> typedTuples, Long limit) {
        AtomicInteger atomicInteger = new AtomicInteger(0);
        List<UserRanking> userRankings = typedTuples.stream().map(x ->
                UserRanking.builder()
                        .ranking(atomicInteger.incrementAndGet())
                        .userId(x.getValue())
                        .score(x.getScore())
                        .updateTime(getUpdateTime(x.getValue()))
                        .build()
        ).collect(Collectors.toList());

        if (userRankings.stream().map(UserRanking::getScore).distinct().count() == typedTuples.size()) {
            // There is no case where the scores are the same
            return userRankings;
        }

        // If the scores are the same, it takes time to do a secondary sort
        return userRankings.stream()
                .sorted(Comparator
                        // The scores are arranged in reverse order, and those with empty scores are at the end
                        .comparing(UserRanking::getScore, Comparator.nullsLast(Double::compareTo)).reversed()
                        // Then they are arranged in positive time order, and those with empty time are at the end
                        .thenComparing(UserRanking::getUpdateTime, Comparator.nullsLast(Long::compareTo)))
                .limit(limit)
                .collect(Collectors.toList());
    }

    /**
     * Get the ranking of a user
     *
     * @param userId
     * @return
     */
    public UserRanking getUserRanking(String userId) {
        Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet().reverseRangeWithScores(key, 0, 0);

        Double score = stringRedisTemplate.opsForZSet().score(key, userId);
        Double maxScore = new ArrayList<>(typedTuples).get(0).getScore();

        Long countOfScore = stringRedisTemplate.opsForZSet().count(key, score, score);
        if (countOfScore == 1) {
            // There is no duplication in my ranking
            Long ranking = stringRedisTemplate.opsForZSet().rank(key, score);

            return UserRanking.builder()
                    .ranking(ranking.intValue() + 1)
                    .userId(userId)
                    .score(score)
                    .updateTime(getUpdateTime(userId))
                    .build();
        } else {
            // My ranking is repeated
            Long countBetweenScores = stringRedisTemplate.opsForZSet().count(key, score, maxScore);

            typedTuples = stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(key, score, score);
            List<UserRanking> userRankings = getRankings(typedTuples, (long) typedTuples.size());
            UserRanking userRanking = userRankings.stream().filter(x -> x.getUserId().equals(userId)).findAny().orElse(null);
            if (userRanking == null) {
                return null;
            }

            // Recalculate ranking information
            userRanking.setRanking((countBetweenScores.intValue() - countOfScore.intValue() + userRanking.getRanking()));
            return userRanking;
        }
    }
}

  I have implemented four methods:

Increase combat power to users

Get all User Rankings

Get the ranking of the specified number of users

Get the ranking of a user

Compared with other implementation methods introduced above, the advantages and disadvantages of my scheme are summarized as follows:

Disadvantages:

The computational logic is slightly complex (but still clear);

Colleagues need to access redis multiple times for each calculation (this does not affect the performance);

You can no longer see ranking information intuitively in redis;

advantage:

The score data stored in redis retains the original value to avoid mixing with the timestamp;

There are few subsequent requirements changes. If you need to add more than two levels of sorting, my modification logic can be realized, but it can't be realized with the above scheme.

Summary:

Record the interesting things in the daily development. Let me old fellow iron company.

Keywords: Redis

Added by mbhcool on Fri, 17 Sep 2021 14:17:33 +0300