排行榜功能是一个比较常见的场景,比如我们平时生活中的用户积分排行榜、用户活跃度排行榜、游戏中的战力排行榜等这些都是排行榜的具体表现。这些场景都有几个基本也是共同点,首先就是并发性很高,在各种场景下排行榜其实都是比较常见并且常用的一种功能,可能每时每刻都有请求去获取排行榜数据;另外一个就是数据实时性要求很高,无论是用户积分还是游戏战力这些数据都是每时每刻都可能在发生变化的。
假如我们使用平时项目中的关系型数据库如MySQL去实现排行榜功能,首先面临第一个问题并发性高很有可能就会造成数据库压力过大、磁盘IO负载太大从而使得数据库无法正常工作,不仅当前排行榜功能会出现问题,还会影响到其他业务;第二个问题要求实时性高,由于数据不停在发生变化,所以不能在DB和应用层之间直接建立缓存,并且对于大数据量来说查询TOP N的数据效率会很低。
这时候Redis
就展现出了的价值了,它里面的SortedSet
有序集合这种数据结构的特性用于实现排行榜简直就是完美契合。这里我们就围绕SortedSet
来编写一个简单的排行榜小项目。
这里我们需要准备一张数据库表
user_score
用于存放用户积分数据,用于数据的恢复以及持久化统计。其实一般还会去维护一张流水表用于数据追踪,这里我们就不做太复杂,实现排行榜的基本功能即可。
CREATE TABLE `user_score` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` varchar(32) NOT NULL,
`user_score` bigint(11) NOT NULL DEFAULT '0',
`user_name` varchar(30) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_user_id` (`user_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8;
设计数据库表的时候一些点大家需要自己去注意和控制,例如:数据类型选择合适、尽量避免Null值、建立合适的索引等。我们通过
mybatis-generator
去生成对应的Entity
和Mapper
,大家也可以自己去编写这个看个人习惯。
另外这里我们需要用到SortSet
的几个方法,不清楚的同学可以扫一眼【Redis - 五种数据类型以及消息发布订阅】。
这里我们主要涉及的几个方法:增加用户积分(zadd、incrementScore)、获取TOP N排行(reverseRangeWithScores、reverseRangeByScoreWithScores),这里我们先贴出代码再把整个流程走一遍。
RankingController.java
package com.springboot.controller;
import com.springboot.service.RankingService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
/**
* @author hzk
* @date 2019/7/22
*/
@RestController
@RequestMapping("/api/ranking/")
public class RankingController {
@Autowired
private RankingService rankingService;
@RequestMapping("/addUser/{userName}")
public String addUser(@PathVariable("userName") String userName){
boolean result = rankingService.addUser(userName);
if(result){
return "添加用户成功";
}else{
return "添加用户失败";
}
}
@RequestMapping("/rankUser/{uid}/{userName}")
public Map<String, Object> rankUser(@PathVariable("uid") String uid,@PathVariable("userName") String userName){
return rankingService.userRank(uid, userName);
}
@RequestMapping("/incrementScore/{uid}/{score}")
public String incrementScore(@PathVariable("uid") String uid,@PathVariable("score") Integer score){
boolean result = rankingService.incrementScore(uid, score);
if(result){
return "添加积分成功";
}else{
return "添加积分失败";
}
}
@RequestMapping("/top/{start}/{end}")
public List<Map<String, Object>> reverseZRankWithRank(@PathVariable("start") long start, @PathVariable("end") long end){
return rankingService.reverseZRankWithRank(start, end);
}
@RequestMapping("/topWithScore/{start}/{end}")
public List<Map<String, Object>> rankWithScore(@PathVariable("start") Integer start, @PathVariable("end") Integer end){
return rankingService.rankWithScore(start, end);
}
}
RankingService.java
package com.springboot.service;
import java.util.List;
import java.util.Map;
/**
* @author hzk
* @date 2019/7/22
*/
public interface RankingService {
/**
* 添加用户
* @param userName
* @return
*/
boolean addUser(String userName);
/**
* 获取用户排名/分数
* @param uid
* @param userName
* @return
*/
Map<String,Object> userRank(String uid, String userName);
/**
* 增加用户分数
* @param uid
* @param score
*/
boolean incrementScore(String uid, Integer score);
/**
* 获取排名(按排名rank)
* @param start
* @param end
* @return
*/
List<Map<String, Object>> reverseZRankWithRank(long start, long end);
/**
* 获取排名(按分数score)
* @param start
* @param end
* @return
*/
List<Map<String, Object>> rankWithScore(Integer start, Integer end);
}
RankingServiceImpl.java
package com.springboot.service.impl;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import com.springboot.dao.UserScoreMapper;
import com.springboot.repository.entity.BlogUser;
import com.springboot.repository.entity.BlogUserExample;
import com.springboot.repository.entity.UserScore;
import com.springboot.repository.entity.UserScoreExample;
import com.springboot.service.RankingService;
import com.springboot.service.RedisService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import javax.annotation.PostConstruct;
import javax.sound.midi.Soundbank;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author hzk
* @date 2019/7/22
*/
@Service
public class RankingServiceImpl implements RankingService {
@Autowired
private UserScoreMapper userScoreMapper;
@Autowired
private RedisService redisService;
private static final String USER_SCORE_RANK = "user_score_rank:";
/**
* 程序启动时初始化用户积分
*/
@PostConstruct
public void initUserScore(){
System.out.println("initUserScore start...");
List<UserScore> userScores = userScoreMapper.selectByExample(new UserScoreExample());
if(!CollectionUtils.isEmpty(userScores)){
userScores.forEach(userScore -> {
String key = userScore.getUserId() + ":" + userScore.getUserName();
redisService.zAdd(USER_SCORE_RANK,key,userScore.getUserScore());
});
}
System.out.println("initUserScore end...");
}
@Override
public boolean addUser(String userName){
boolean result = false;
UserScoreExample userScoreExample = new UserScoreExample();
userScoreExample.createCriteria().andUserNameEqualTo(userName);
List<UserScore> userScores = userScoreMapper.selectByExample(userScoreExample);
if(CollectionUtils.isEmpty(userScores)){
UserScore userScore = new UserScore();
userScore.setUserName(userName);
userScore.setUserScore(0L);
userScore.setUserId(StringUtils.replace(java.util.UUID.randomUUID().toString(), "-", "").toUpperCase());
int resultFlag = userScoreMapper.insertSelective(userScore);
if(resultFlag > 0){
result = true;
}
}
return result;
}
@Override
public Map<String,Object> userRank(String uid, String userName){
Map<String, Object> retMap = new LinkedHashMap<>();
String key = uid + ":" + userName;
Integer rank = redisService.zRank(USER_SCORE_RANK, key).intValue();
Long score = redisService.zSetScore(USER_SCORE_RANK, key).longValue();
retMap.put("userId", uid);
retMap.put("score", score);
retMap.put("rank", rank);
return retMap;
}
@Override
public boolean incrementScore(String uid, Integer score) {
boolean result = false;
UserScoreExample userScoreExample = new UserScoreExample();
userScoreExample.createCriteria().andUserIdEqualTo(uid);
List<UserScore> userScores = userScoreMapper.selectByExample(userScoreExample);
if(CollectionUtils.isEmpty(userScores)){
return result;
}
UserScore userScore = userScores.get(0);
long scoreLong = Long.parseLong(score + "");
String name = userScore.getUserName();
String key = uid + ":" + name;
userScore.setUserScore(userScore.getUserScore() + scoreLong);
redisService.incrementScore(USER_SCORE_RANK, key, score);
int resultFlag = userScoreMapper.updateByPrimaryKeySelective(userScore);
if(resultFlag > 0){
result = true;
}
return result;
}
@Override
public List<Map<String, Object>> reverseZRankWithRank(long start, long end) {
Set<ZSetOperations.TypedTuple<Object>> setObj = redisService.reverseZRankWithRank(USER_SCORE_RANK, start, end);
return setObj.stream().map(objectTypedTuple -> {
Map<String, Object> map = new LinkedHashMap<>();
map.put("userId", objectTypedTuple.getValue().toString().split(":")[0]);
map.put("userName", objectTypedTuple.getValue().toString().split(":")[1]);
map.put("score", objectTypedTuple.getScore());
return map;
}).collect(Collectors.toList());
}
@Override
public List<Map<String, Object>> rankWithScore(Integer start, Integer end) {
Set<ZSetOperations.TypedTuple<Object>> setObj = redisService.reverseZRankWithScore(USER_SCORE_RANK, start, end);
return setObj.stream().map(objectTypedTuple -> {
Map<String, Object> map = new LinkedHashMap<>();
map.put("userId", objectTypedTuple.getValue().toString().split(":")[0]);
map.put("userName", objectTypedTuple.getValue().toString().split(":")[1]);
map.put("score", objectTypedTuple.getScore());
return map;
}).collect(Collectors.toList());
}
}
RedisService.java
package com.springboot.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Service;
import java.io.Serializable;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* @author hzk
* @date 2019/7/1
*/
@Service
public class RedisService {
@Autowired
private RedisTemplate redisTemplate;
private static double size = Math.pow(2, 32);
/**
* 写入缓存
* @param key
* @param offset 位 8Bit=1Byte
* @return
*/
public boolean setBit(String key, long offset, boolean isShow) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.setBit(key, offset, isShow);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 读取缓存
* @param key
* @param offset
* @return
*/
public boolean getBit(String key, long offset) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
result = operations.getBit(key, offset);
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 写入缓存(String)
* @param key
* @param value
* @return
*/
public boolean set(final String key, Object value) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 设置失效时间
* @param key
* @param value
* @return
*/
public boolean setExpire(final String key, Object value, Long expireTime) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 批量删除对应的key
* @param keys
*/
public void remove(final String... keys) {
for (String key : keys) {
remove(key);
}
}
/**
* 删除对应的key
* @param key
*/
public void remove(final String key) {
if (exists(key)) {
redisTemplate.delete(key);
}
}
/**
* 判断缓存中是否有对应的key
* @param key
* @return
*/
public boolean exists(final String key) {
return redisTemplate.hasKey(key);
}
/**
* 读取缓存(String)
* @param key
* @return
*/
public Object get(final String key) {
Object result = null;
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
result = operations.get(key);
return result;
}
/**
* 写入缓存(哈希Hash)
* @param key
* @param hashKey
* @param value
*/
public void hmSet(String key, Object hashKey, Object value) {
HashOperations<String, Object, Object> hash = redisTemplate.opsForHash();
hash.put(key, hashKey, value);
}
/**
* 读取缓存(哈希Hash)
*
* @param key
* @param hashKey
* @return
*/
public Object hmGet(String key, Object hashKey) {
HashOperations<String, Object, Object> hash = redisTemplate.opsForHash();
return hash.get(key, hashKey);
}
/**
* 写入缓存(列表List)
* @param key
* @param value
*/
public void lPush(String key, Object value) {
ListOperations<String, Object> list = redisTemplate.opsForList();
list.rightPush(key, value);
}
/**
* 读取缓存(列表List)
* @param key
* @param start
* @param end
* @return
*/
public List<Object> lRange(String key, long start, long end) {
ListOperations<String, Object> list = redisTemplate.opsForList();
return list.range(key, start, end);
}
/**
* 写入缓存(集合Set)
* @param key
* @param value
*/
public void add(String key, Object value) {
SetOperations<String, Object> set = redisTemplate.opsForSet();
set.add(key, value);
}
/**
* 获取缓存(集合Set)
* @param key
* @return
*/
public Set<Object> setMembers(String key) {
SetOperations<String, Object> set = redisTemplate.opsForSet();
return set.members(key);
}
/**
* 写入缓存(有序集合SortedSet)
* @param key
* @param value
* @param score
*/
public void zAdd(String key, Object value, double score) {
ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
zset.add(key, value, score);
}
/**
* 读取范围内缓存(有序集合SortedSet)
* @param key
* @param minScore
* @param maxScore
* @return
*/
public Set<Object> rangeByScore(String key, double minScore, double maxScore) {
ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
redisTemplate.opsForValue();
return zset.rangeByScore(key, minScore, maxScore);
}
/**
* 获取排名(有序集合SortedSet)
* @param key 集合名称
* @param value 值
*/
public Long zRank(String key, Object value) {
ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
return zset.rank(key,value);
}
/**
* 获取范围内排名(有序集合SortedSet)
* @param key
*/
public Set<ZSetOperations.TypedTuple<Object>> zRankWithScore(String key, long start,long end) {
ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
Set<ZSetOperations.TypedTuple<Object>> ret = zset.rangeWithScores(key,start,end);
return ret;
}
/**
* 获取分数(有序集合SortedSet)
* @param key
* @param value
*/
public Double zSetScore(String key, Object value) {
ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
return zset.score(key,value);
}
/**
* 更新分数(有序集合SortedSet)
* @param key
* @param value
* @param score
*/
public void incrementScore(String key, Object value, double score) {
ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
zset.incrementScore(key, value, score);
}
/**
* 有序集合获取排名
*
* @param key
*/
public Set<ZSetOperations.TypedTuple<Object>> reverseZRankWithScore(String key, long start, long end) {
ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
Set<ZSetOperations.TypedTuple<Object>> ret = zset.reverseRangeByScoreWithScores(key,start,end);
return ret;
}
/**
* 有序集合获取排名
*
* @param key
*/
public Set<ZSetOperations.TypedTuple<Object>> reverseZRankWithRank(String key, long start, long end) {
ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
Set<ZSetOperations.TypedTuple<Object>> ret = zset.reverseRangeWithScores(key, start, end);
return ret;
}
//第一次加载的时候将数据加载到redis中
public void saveDataToRedis(String name) {
double index = Math.abs(name.hashCode() % size);
long indexLong = new Double(index).longValue();
boolean availableUsers = setBit("availableUsers", indexLong, true);
}
//第一次加载的时候将数据加载到redis中
public boolean getDataToRedis(String name) {
double index = Math.abs(name.hashCode() % size);
long indexLong = new Double(index).longValue();
return getBit("availableUsers", indexLong);
}
}
编写好项目之后,接下来我们就来把整个流程走一遍看看是否符合我们的基本需求。首先我们通过
/api/ranking/addUser/{userName}
接口新增了一批数据。
接着我们重启项目通过RankingServiceImpl
中的initUserScore()
方法将我们数据库中所存在的用户信息以及积分放入Redis
中用于之后的操作。
我们先通过/api/ranking/incrementScore/E988FF491A244864BE679EF0255F7195/1
给wade
先加上一分,然后通过/api/ranking/rankUser/E988FF491A244864BE679EF0255F7195/wade
可以查看到当前用户的积分状况。
这里可以看到当前用户的积分为1,排名为6,这是由于SortedSet
的排名是从低到高的。这里我们先通过上面增加积分的接口给每个用户加上不等的积分。
这里我们通过/api/ranking/top/0/2
获取TOP 3的排名数据。
若我们要获取所有排行数据,只需将参数调整为-1即可/api/ranking/top/0/-1
。
另外我们可以通过/api/ranking/topWithScore/5/10
获取积分是5-10之间的排名数据。
上面我们编写了initUserScore()方法通过@PostConstruct注解去实现初始化加载数据。我们还可以通过其他方式来实现初始化数据缓存预热。
第一种是实现ApplicationRunner
,该方式在SpringApplication.run(…)完成之前调用,例如下面代码当初始化完成之前用户是可以正常通过请求访问接口的。
@Override
public void run(ApplicationArguments args) throws Exception {
Thread.sleep(100000);
this.initUserScore();
}
第二种方式是实现
InitializingBean
,该方式在Spring初始化Bean的时候若Bean实现了InitializingBean
接口重写了afterPropertiesSet()
方法,则在完成初始化之前用户无法正常访问接口。
@Override
public void afterPropertiesSet() throws Exception {
Thread.sleep(100000);
this.initUserScore();
}
具体大家根据自己需求去选择,整个排行榜的功能这里就完成了简单的实现。