场景
在互金领域中公司为了拉动用户投资,推出投资擂台赛活动(活动期间用户的总投资金额PK),在游戏领域中会有玩家等级排行榜,记步软件如 微信运动/支付宝-运动 中行走步数排行榜。
设计思路
说到排行榜就不得不说Redis 提供的 有序集合SortedSet数据结构。
Redis 有序集合和集合一样也是string类型元素的集合且不允许重复的成员,不同的是每个元素都会关联一个double类型的分数。redis通过分数来为集合中的成员进行从小到大的排序。
简而言之,一共三步:
- 通过redis
ZADD key score member命令添加用户步数到SortedSet;
- 通过 redis
ZRANK key member 命令获取某个用户在排行榜中到名次;
- 通过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
}
场景
在互金领域中公司为了拉动用户投资,推出投资擂台赛活动(活动期间用户的总投资金额PK),在游戏领域中会有玩家等级排行榜,记步软件如 微信运动/支付宝-运动 中行走步数排行榜。
设计思路
说到排行榜就不得不说Redis 提供的 有序集合SortedSet数据结构。
Redis 有序集合和集合一样也是string类型元素的集合且不允许重复的成员,不同的是每个元素都会关联一个double类型的分数。redis通过分数来为集合中的成员进行从小到大的排序。
简而言之,一共三步:
ZADD key score member命令添加用户步数到SortedSet;ZRANK key member命令获取某个用户在排行榜中到名次;ZRANGE key start stop [WITHSCORES]命令获取Top N用户列表。代码实现
本篇以 微信运动中的步数排行榜为例 进行讲解。
maven依赖
1.更新用户步数
2.获取用户在排行榜中的排名
3.获取排行榜列表
4. 单元测试
5.完整代码
WechatStepRankingService.java
User.java
RankingItem.java