Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

排行榜系统设计 #21

Open
TFdream opened this issue Oct 11, 2018 · 0 comments
Open

排行榜系统设计 #21

TFdream opened this issue Oct 11, 2018 · 0 comments

Comments

@TFdream
Copy link
Owner

TFdream commented Oct 11, 2018

场景

在互金领域中公司为了拉动用户投资,推出投资擂台赛活动(活动期间用户的总投资金额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
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant