SpringBoot实战应⽤之如何借助Redis实现排⾏榜功能
在⼀些游戏和活动中,当涉及到社交元素的时候,排⾏榜可以说是⼀个很常见的需求场景了,就我们通常见到的排⾏榜⽽⾔,会提供以下基本功能
全球榜单,对所有⽤户根据积分进⾏排名,并在榜单上展⽰前多少
个⼈排名,⽤户查询⾃⼰所在榜单的位置,并获知周边⼩伙伴的积分,⽅便⾃⼰⽐较和超越
实时更新,⽤户的积分实时更改,榜单也需要实时更新
上⾯可以说是⼀个排⾏榜需要实现的⼏个基本要素了,正好我们刚讲到了redis这⼀节,本篇则开始实战,详细描述如何借助redis来实现⼀份全球排⾏榜
I. ⽅案设计
在进⾏⽅案设计之前,先模拟⼀个真实的应⽤场景,然后进⾏辅助设计与实现
1. 业务场景说明
以前⼀段时间特别的跳⼀跳这个⼩游戏进⾏说明,假设我们这个游戏⽤户遍布全球,因此我们要设计⼀
个全球的榜单,每个玩家都会根据⾃⼰的战绩在排⾏榜中获取⼀个排名,我们需要⽀持全球榜单的查询,⾃⼰排位的查询这两种最基本的查询场景;此外当我的分数⽐上⼀次的⾼时,我需要更新我的积分,重新获得我的排名;
此外也会有⼀些⾼级的统计,⽐如哪个分段的⼈数最多,什么分段是瓶颈点,再根据地理位置计算平均分等等
本篇博⽂主要内容将放在排⾏榜的设计与实现上;⾄于⾼级的功能实现,后续有机会再说
2. 数据结构
因为排⾏榜的功能⽐较简单了,也不需要什么复杂的结构设计,也没有什么复杂的交互,因此我们需要确认的⽆⾮就是数据结构 + 存储单元
存储单元
表⽰排⾏榜中每⼀位上应该持有的信息,⼀个最简单的如下
// ⽤来表明具体的⽤户long userId;// ⽤户在排⾏榜上的排名long rank;// ⽤户的历史最⾼积分,也就是排⾏榜上的积分long score;
数据结构
排⾏榜,⼀般⽽⾔都是连续的,借此我们可以联想到⼀个合适的数据结构LinkedList,好处在于排名变动时,不需要数组的拷贝
SpringBoot实战应⽤之如何借助Redis实现排⾏榜功能
上图演⽰,当⼀个⽤户积分改变时,需要向前遍历到合适的位置,插⼊并获取新的排名, 在更新和插⼊时,相⽐较于ArrayList要好很多,但依然有以下⼏个缺陷
问题1:⽤户如何获取⾃⼰的排名?
使⽤LinkedList在更新插⼊和删除的带来优势之外,在随机获取元素的⽀持会差⼀点,最差的情况就是从头到尾进⾏扫描
问题2:并发⽀持的问题?
当有多个⽤户同时更新score时,并发的更新排名问题就⽐较突出了,当然可以使⽤jdk中类似写时拷贝数组的⽅案
上⾯是我们⾃⼰来实现这个数据结构时,会遇到的⼀些问题,当然我们的主题是借助redis来实现排⾏榜,下⾯则来看下,利⽤redis可以怎么简单的⽀持我们的需求场景
3. redis使⽤⽅案
这⾥主要使⽤的是redis的ZSET数据结构,带权重的集合,下⾯分析⼀下可能性
set: 集合确保⾥⾯元素的唯⼀性
权重:这个可以看做我们的score,这样每个元素都有⼀个score;
zset:根据score进⾏排序的集合
从zset的特性来看,我们每个⽤户的积分,丢到zset中,就是⼀个带权重的元素,⽽且是已经排好序的了,只需要获取元素对应的index,就是我们预期的排名
II. 功能实现
再具体的实现之前,可以先查看⼀下redis中zset的相关⽅法和操作姿势:SpringBoot⾼级篇Redis之ZSet数据结构使⽤姿势
我们主要是借助zset提供的⼀些⽅法来实现排⾏榜的需求,下⾯的具体⽅法设计中,也会有相关说明
0. 前提准备
⾸先准备好redis环境,spring项⽬搭建好,然后配置好redisTemplate
/** * Created by @author yihui in 15:05 18/11/8. */public class DefaultSerializer implements RedisSerializer { private final Charset charset; public DefaultSerializer() { this(Charset.forName(“UTF8”)); } public DefaultSerializer(Charset charset) { Null(charset, “Charset must not be null!”); 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);
}}@Configurationpublic 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(stringRedisS
erializer);
springboot结构
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);}
上⾯的封装中,除了使⽤前⾯的获取⽤户排名之外,还有获取⽤户积分
/** * 查询value对应的score zscore * * @param key * @param value * @return */public Double score(String key, String value) { return redisTemplate.opsForZSet().score(key, value);}
3. 获取个⼈周边⽤户积分及排⾏信息
有了前⾯的基础之后,这个就⽐较简单了,⾸先获取⽤户的个⼈排名,然后查询固定排名段的数据即可
private List buildRedisRankToBizDO(Set<ZSetOperations.TypedTuple> result, long offset) { List rankList = new ArrayList<> (result.size()); long rank = offset; for (ZSetOperations.TypedTuple sub : result) { rankList.add(new RankDO(rank++,
Math.Score().floatValue()), Long.Value()))); } return rankList;}/** * 获取⽤户所在排⾏榜的位置,以及排⾏榜中其前后n个⽤户的排⾏信息 * * @param userId * @param n * @return */public List getRankAroundUser(Long userId,
int n) { // ⾸先是获取⽤户对应的排名 RankDO rank = getRank(userId); if (Rank() <= 0) { // fixme ⽤户没有上榜时,不返回 ptyList(); } // 因为实际的排名是从0开始的,所以查询周边排名时,需要将n-1
Set<ZSetOperations.TypedTuple> 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 getTopNRanks(int n) { Set<ZSetOperations.TypedTuple> result = redisComponent.rangeWithScore(RANK_PREFIX, 0, n - 1); return buildRedisRankToBizDO(result, 1);}
III. 测试⼩结
⾸先准备⼀个测试脚本,批量的插⼊⼀下积分,⽤于后续的查询更新使⽤
1. 测试
上⾯执⾏完毕之后,排⾏榜中应该就有三⼗条数据,接下来我们开始逐个接⼝测试,⾸先获取top10排⾏
对应的rest接⼝如下
@RestControllerpublic class RankAction { @Autowired private RankListComponent rankListComponent; @GetMapping(path = “/topn”) public List showTopN(int n) { TopNRanks(n); }}
SpringBoot实战应⽤之如何借助Redis实现排⾏榜功能
接下来我们挑选第15名,获取对应的排⾏榜信息
@GetMapping(path = “/rank”)public RankDO queryRank(long userId) { Rank(userId);}
⾸先我们从redis中获取第15名的userId,然后再来查询
SpringBoot实战应⽤之如何借助Redis实现排⾏榜功能
然后尝试修改下他的积分,改⼤⼀点,将score改成80分,则会排到第五名
@GetMapping(path = “/update”)public RankDO updateScore(long userId, float score) { return
rankListComponent.updateRank(userId, score);}
SpringBoot实战应⽤之如何借助Redis实现排⾏榜功能
最后我们查询下这个⽤户周边2个的排名信息
@GetMapping(path = “/around”)public List around(long userId, int n) { return
SpringBoot实战应⽤之如何借助Redis实现排⾏榜功能
2. ⼩结
上⾯利⽤redis的zset实现了排⾏榜的基本功能,主要借助下⾯三个⽅法
range 获取范围排⾏信息
score 获取对应的score
range 获取对应的排名
虽然实现了基本功能,但是问题还是有不少的
上⾯的实现,redis的复合操作,原⼦性问题
由原⼦性问题导致并发安全问题
性能怎么样需要测试
最后,如果觉得本⽂不错,那就关注转发⼀下吧!

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。