Skip to content

排行榜系统设计 #21

Description

@TFdream

场景

在互金领域中公司为了拉动用户投资,推出投资擂台赛活动(活动期间用户的总投资金额PK),在游戏领域中会有玩家等级排行榜,记步软件如 微信运动/支付宝-运动 中行走步数排行榜。

设计思路

说到排行榜就不得不说Redis 提供的 有序集合SortedSet数据结构。

Redis 有序集合和集合一样也是string类型元素的集合且不允许重复的成员,不同的是每个元素都会关联一个double类型的分数。redis通过分数来为集合中的成员进行从小到大的排序。

简而言之,一共三步:

  1. 通过redis ZADD key score member命令添加用户步数到SortedSet;
  2. 通过 redis ZRANK key member 命令获取某个用户在排行榜中到名次;
  3. 通过redis ZRANGE key start stop [WITHSCORES] 命令获取Top N用户列表。

代码实现

本篇以 微信运动中的步数排行榜为例 进行讲解。

maven依赖

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
    </dependencies>

1.更新用户步数

    /**
     * 更新用户步数
     * @param user
     * @param date
     * @param step
     */
    public void updateUserStep(User user, Date date, int step) {
        String key = getRankingKey(date);
        //计算score,为了让步数大的排在前面
        double score = MAX_STEP - step;
        stringRedisTemplate.opsForZSet().add(key, serializeUser(user), score);
    }

2.获取用户在排行榜中的排名

    /**
     * 获取用户在排行榜中的排名
     * @param user
     * @param date
     */
    public long getUserRanking(User user, Date date) {
        String key = getRankingKey(date);
        return stringRedisTemplate.opsForZSet().rank(key, serializeUser(user)).longValue() + 1;
    }

3.获取排行榜列表

    /**
     * 获取排行榜列表
     * @param date
     * @param num
     * @return
     */
    public List<RankingItem> getTopN(Date date, int num) {
        String key = getRankingKey(date);
        Set<ZSetOperations.TypedTuple<String>> tuples = stringRedisTemplate.opsForZSet().rangeWithScores(key, 0, num);
        if (CollectionUtils.isEmpty(tuples)) {
            return Collections.emptyList();
        }
        List<RankingItem> rankingList = new ArrayList<>(tuples.size());
        for (ZSetOperations.TypedTuple<String> tuple: tuples) {

            RankingItem item = new RankingItem();
            User user = deserializeUser(tuple.getValue());
            item.setUserId(user.getId());
            item.setNickname(user.getNickname());
            //计算用户步数
            Double score = tuple.getScore();
            item.setStep(MAX_STEP - score.intValue());

            rankingList.add(item);
        }
        return rankingList;
    }

4. 单元测试

/**
 * @author Ricky Fung
 */
public class WechatStepRankingServiceTest extends BaseSpringJUnitTest {

    @Resource(name = "wechatStepRankingService")
    private WechatStepRankingService wechatStepRankingService;

    @Test
    public void testUpdateRanking() {
        Date now = new Date();
        int step = 2000;
        for (int i=0; i<100; i++) {
            User user = new User();
            user.setId(Long.valueOf(i));
            user.setNickname("ws"+i);
            wechatStepRankingService.updateUserStep(user, now, step+i);
        }
    }

    @Test
    public void testRankingList() {
        Date now = new Date();
        List<RankingItem> list = wechatStepRankingService.getTopN(now, 20);
        System.out.println(JsonUtils.toJson(list));
    }

    @Test
    public void testUserRanking() {
        Date now = new Date();
        User user = new User();
        Long userId = 98L;
        user.setId(userId);
        user.setNickname("ws"+userId);
        long rank = wechatStepRankingService.getUserRanking(user, now);
        System.out.println(rank);
    }
}

5.完整代码

WechatStepRankingService.java

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.*;

/**
 * 微信运动-步数排行榜
 * @author Ricky Fung
 */
@Service
public class WechatStepRankingService {

    /**
     * 用户每天步数上限:100万步
     */
    private static final int MAX_STEP = 1000000;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 更新用户步数
     * @param user
     * @param date
     * @param step
     */
    public void updateUserStep(User user, Date date, int step) {
        String key = getRankingKey(date);
        //计算score,为了让步数大的排在前面
        double score = MAX_STEP - step;
        stringRedisTemplate.opsForZSet().add(key, serializeUser(user), score);
    }

    /**
     * 获取排行榜列表
     * @param date
     * @param num
     * @return
     */
    public List<RankingItem> getTopN(Date date, int num) {
        String key = getRankingKey(date);
        Set<ZSetOperations.TypedTuple<String>> tuples = stringRedisTemplate.opsForZSet().rangeWithScores(key, 0, num);
        if (CollectionUtils.isEmpty(tuples)) {
            return Collections.emptyList();
        }
        List<RankingItem> rankingList = new ArrayList<>(tuples.size());
        for (ZSetOperations.TypedTuple<String> tuple: tuples) {

            RankingItem item = new RankingItem();
            User user = deserializeUser(tuple.getValue());
            item.setUserId(user.getId());
            item.setNickname(user.getNickname());
            //计算用户步数
            Double score = tuple.getScore();
            item.setStep(MAX_STEP - score.intValue());

            rankingList.add(item);
        }
        return rankingList;
    }

    /**
     * 获取用户排行榜排名
     * @param user
     * @param date
     */
    public long getUserRanking(User user, Date date) {
        String key = getRankingKey(date);
        return stringRedisTemplate.opsForZSet().rank(key, serializeUser(user)).longValue() + 1;
    }

    private String getRankingKey(Date date) {
        return String.format("%s:%s", "wechat:rank", DateUtils.formatDate(date));
    }

    //----------
    private String serializeUser(User user) {
        return String.format("%s#%s", user.getId(), user.getNickname());
    }

    private User deserializeUser(String str) {
        String[] arr = str.split("#");
        User user = new User();
        user.setId(Long.parseLong(arr[0]));
        user.setNickname(arr[1]);
        return user;
    }
}

User.java

public class User {
    private Long id;
    private String nickname;

    //省略 getter/setter
    
}

RankingItem.java

public class RankingItem {
    private Long userId;
    private String nickname;
    private int step;

    //省略 getter/setter
}

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions