Skip to content
New issue

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

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

Already on GitHub? # 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
}
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant