SpringBoot应⽤篇之借助Redis实现排⾏榜功能更多Spring⽂章,欢迎点击
在⼀些游戏和活动中,当涉及到社交元素的时候,排⾏榜可以说是⼀个很常见的需求场景了,就我们通常见到的排⾏榜⽽⾔,会提供以下基本功能
全球榜单,对所有⽤户根据积分进⾏排名,并在榜单上展⽰前多少
个⼈排名,⽤户查询⾃⼰所在榜单的位置,并获知周边⼩伙伴的积分,⽅便⾃⼰⽐较和超越
实时更新,⽤户的积分实时更改,榜单也需要实时更新
上⾯可以说是⼀个排⾏榜需要实现的⼏个基本要素了,正好我们刚讲到了redis这⼀节,本篇则开始实战,详细描述如何借助redis来实现⼀份全球排⾏榜
I. ⽅案设计
在进⾏⽅案设计之前,先模拟⼀个真实的应⽤场景,然后进⾏辅助设计与实现
1. 业务场景说明
以前⼀段时间特别?的跳⼀跳这个⼩游戏进⾏说明,假设我们这个游戏⽤户遍布全球,因此我们要设计⼀
个全球的榜单,每个玩家都会根据⾃⼰的战绩在排⾏榜中获取⼀个排名,我们需要⽀持全球榜单的查询,⾃⼰排位的查询这两种最基本的查询场景;此外当我的分数⽐上⼀次的⾼时,我需要更新我的积分,重新获得我的排名;
此外也会有⼀些⾼级的统计,⽐如哪个分段的⼈数最多,什么分段是瓶颈点,再根据地理位置计算平均分等等
本篇博⽂主要内容将放在排⾏榜的设计与实现上;⾄于⾼级的功能实现,后续有机会再说
2. 数据结构
因为排⾏榜的功能⽐较简单了,也不需要什么复杂的结构设计,也没有什么复杂的交互,因此我们需要确认的⽆⾮就是数据结构 + 存储单元
存储单元
表⽰排⾏榜中每⼀位上应该持有的信息,⼀个最简单的如下
// ⽤来表明具体的⽤户
long userId;
// ⽤户在排⾏榜上的排名
long rank;
// ⽤户的历史最⾼积分,也就是排⾏榜上的积分
long score;
数据结构
排⾏榜,⼀般⽽⾔都是连续的,借此我们可以联想到⼀个合适的数据结构LinkedList,好处在于排名变动时,不需要数组的拷贝
上图演⽰,当⼀个⽤户积分改变时,需要向前遍历到合适的位置,插⼊并获取新的排名, 在更新和插⼊时,相⽐较于ArrayList要好很多,但依然有以下⼏个缺陷
问题1:⽤户如何获取⾃⼰的排名?
使⽤LinkedList在更新插⼊和删除的带来优势之外,在随机获取元素的⽀持会差⼀点,最差的情况就是从头到尾进⾏扫描
问题2:并发⽀持的问题?
当有多个⽤户同时更新score时,并发的更新排名问题就⽐较突出了,当然可以使⽤jdk中类似写时拷贝数组的⽅案
上⾯是我们⾃⼰来实现这个数据结构时,会遇到的⼀些问题,当然我们的主题是借助redis来实现排⾏榜,下⾯则来看下,利⽤redis可以怎么简单的⽀持我们的需求场景
3. redis使⽤⽅案
这⾥主要使⽤的是redis的ZSET数据结构,带权重的集合,下⾯分析⼀下可能性
set: 集合确保⾥⾯元素的唯⼀性
权重:这个可以看做我们的score,这样每个元素都有⼀个score;
zset:根据score进⾏排序的集合
从zset的特性来看,我们每个⽤户的积分,丢到zset中,就是⼀个带权重的元素,⽽且是已经排好序的了,只需要获取元素对应的index,就是我们预期的排名
II. 功能实现
再具体的实现之前,可以先查看⼀下redis中zset的相关⽅法和操作姿势:
我们主要是借助zset提供的⼀些⽅法来实现排⾏榜的需求,下⾯的具体⽅法设计中,也会有相关说明
0. 前提准备
⾸先准备好redis环境,spring项⽬搭建好,然后配置好redisTemplate
/**
* Created by @author yihui in 15:05 18/11/8.
*/
public class DefaultSerializer implements RedisSerializer<Object>{
private final Charset charset;
public DefaultSerializer(){
this(Charset.forName("UTF8"));
}
public DefaultSerializer(Charset charset){
this.charset = charset;
}
@Override
public byte[]serialize(Object o)throws SerializationException {
return o == null ? null : String.valueOf(o).getBytes(charset);
}
@Override
public Object deserialize(byte[] bytes)throws SerializationException {
return bytes == null ? null :new String(bytes, charset);
}
}
@Configuration
public class AutoConfig {
@Bean(value ="selfRedisTemplate")
public RedisTemplate<String, String>stringRedisTemplate(RedisConnectionFactory redisConnectionFactory){
StringRedisTemplate redis =new StringRedisTemplate();
redis.setConnectionFactory(redisConnectionFactory);
// 设置redis的String/Value的默认序列化⽅式
DefaultSerializer stringRedisSerializer =new DefaultSerializer();
redis.setKeySerializer(stringRedisSerializer);
redis.setValueSerializer(stringRedisSerializer);
redis.setHashKeySerializer(stringRedisSerializer);
redis.setHashValueSerializer(stringRedisSerializer);
redis.afterPropertiesSet();
return redis;
}
}
1. ⽤户上传积分
上传⽤户积分,然⽽zset中有⼀点需要注意的是其排⾏是根据score进⾏升序排列,这个就和我们实际的情况不太⼀样了;为了和实际情况⼀致,可以将score取反;另外⼀个就是排⾏默认是从0开始的,这个与我们的实际也不太⼀样,需要+1
/**
* 更新⽤户积分,并获取最新的个⼈所在排⾏榜信息
*
* @param userId
* @param score
* @return
*/
public RankDO updateRank(Long userId, Float score){
// 因为zset默认积分⼩的在前⾯,所以我们对score进⾏取反,这样⽤户的积分越⼤,对应的score越⼩,排名越⾼
redisComponent.add(RANK_PREFIX, String.valueOf(userId),-score);
Long rank = redisComponent.rank(RANK_PREFIX, String.valueOf(userId));
return new RankDO(rank +1, score, userId);
}
上⾯的实现,主要利⽤了zset的两个⽅法,⼀个是添加元素,⼀个是查询排名,对应的redis操作⽅法如下,
@Resource(name ="selfRedisTemplate")
private StringRedisTemplate redisTemplate;
/**
* 添加⼀个元素, zset与set最⼤的区别就是每个元素都有⼀个score,因此有个排序的辅助功能; zadd
*
* @param key
* @param value
* @param score
*/
public void add(String key, String value,double score){
redisTemplate.opsForZSet().add(key, value, score);
}
/**
* 判断value在zset中的排名 zrank
*
* 积分⼩的在前⾯
*
* @param key
* @param value
* @return
*/
public Long rank(String key, String value){
return redisTemplate.opsForZSet().rank(key, value);
}
2. 获取个⼈排名
获取个⼈排⾏信息,主要就是两个⼀个是排名⼀个是积分;需要注意的是当⽤户没有积分时(即没有上榜时),需要额外处理
/**
* 获取⽤户的排⾏榜位置
*
* @param userId
* @return
*/
public RankDO getRank(Long userId){
// 获取排⾏,因为默认是0为开头,因此实际的排名需要+1
Long rank = redisComponent.rank(RANK_PREFIX, String.valueOf(userId));
if(rank == null){
// 没有排⾏时,直接返回⼀个默认的
return new RankDO(-1L,0F, userId);
}
// 获取积分
Double score = redisComponent.score(RANK_PREFIX, String.valueOf(userId));
return new RankDO(rank +1, Math.abs(score.floatValue()), userId);
}
redis支持的数据结构上⾯的封装中,除了使⽤前⾯的获取⽤户排名之外,还有获取⽤户积分
/**
* 查询value对应的score zscore
*
* @param key
* @param value
* @return
*/
public Double score(String key, String value){
return redisTemplate.opsForZSet().score(key, value);
}
3. 获取个⼈周边⽤户积分及排⾏信息
有了前⾯的基础之后,这个就⽐较简单了,⾸先获取⽤户的个⼈排名,然后查询固定排名段的数据即可
private List<RankDO>buildRedisRankToBizDO(Set<ZSetOperations.TypedTuple<String>> result,long offset){
List<RankDO> rankList =new ArrayList<>(result.size());
long rank = offset;
for(ZSetOperations.TypedTuple<String> sub : result){
rankList.add(new RankDO(rank++, Math.Score().floatValue()), Long.Value())));
}
return rankList;
}
/**
* 获取⽤户所在排⾏榜的位置,以及排⾏榜中其前后n个⽤户的排⾏信息
*
* @param userId
* @param n
* @return
*/
public List<RankDO>getRankAroundUser(Long userId,int n){
// ⾸先是获取⽤户对应的排名
RankDO rank =getRank(userId);
Rank()<=0){
// fixme ⽤户没有上榜时,不返回
ptyList();
}
// 因为实际的排名是从0开始的,所以查询周边排名时,需要将n-1
Set<ZSetOperations.TypedTuple<String>> result =
redisComponent.rangeWithScore(RANK_PREFIX, Math.max(0, Rank()- n -1), Rank()+ n -1);
return buildRedisRankToBizDO(result, Rank()- n);
}
看下上⾯的实现,获取⽤户排名之后,就可以计算要查询的排名范围[Math.max(0, Rank() - n - 1), Rank() + n - 1]其次需要注意的如何将返回的结果进⾏封装,上⾯写了个转换类,主要起始排⾏榜信息
4. 获取topn排⾏榜
上⾯的理解之后,这个就很简答了
/**
* 获取前n名的排⾏榜数据
*
* @param n
* @return
*/
public List<RankDO>getTopNRanks(int n){
Set<ZSetOperations.TypedTuple<String>> result = redisComponent.rangeWithScore(RANK_PREFIX,0, n -1);
return buildRedisRankToBizDO(result,1);
}
III. 测试⼩结
⾸先准备⼀个测试脚本,批量的插⼊⼀下积分,⽤于后续的查询更新使⽤
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论