一、背景
前段时间接了一个需求,需要做个飞机大战H5小游戏,游戏中要根据用户的积分进行排名,做个排行榜;
![](https://i-blog.csdnimg.cn/blog_migrate/1a4967e45404b958b0ad50de19de7b52.png)
用户的积分保存在积分表中如下图,用户最新积分超过之前积分就更新用户的积分;
![](https://i-blog.csdnimg.cn/blog_migrate/7c1556d72056b23b733fa21a07c044a2.png)
![](https://i-blog.csdnimg.cn/blog_migrate/041b32acdebb08e1c8b5d84f3f6bd7fa.png)
如果简单的来做,那么可以直接根据“best_result”字段 做倒序排序,在使用limit就可以控制前几名了,但是这样做一是我没有显示的标记出哪一个是第几名,只能根据”第一个是第一名,第二个是第二名.....“的方式,去排序;这样肯定不行;
二、MySQL实现排行榜
ps: 这一部分是我百度看到的,自己实操了一遍,放在这里主要是为了引出,下面的Redis实现,求审核放过 TvT~~~~ TvT~~~~
如果要使用MySql去是实现有下面的实现,为了方便测试,我们先创建一个表
#表结构
CREATE TABLE `user_integral` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL,
`integral` int(50) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8
#初始数据
INSERT INTO user_integral (name, integral) VALUES('AA', 123);
INSERT INTO user_integral (name, integral) VALUES('BB', 1235);
INSERT INTO user_integral (name, integral) VALUES('CC', 345);
INSERT INTO user_integral (name, integral) VALUES('DD', 556);
INSERT INTO user_integral (name, integral) VALUES('EE', 879);
INSERT INTO user_integral (name, integral) VALUES('FF', 3456);
INSERT INTO user_integral (name, integral) VALUES('GG', 1000);
INSERT INTO user_integral (name, integral) VALUES('HH', 212);
INSERT INTO user_integral (name, integral) VALUES('II', 111);
INSERT INTO user_integral (name, integral) VALUES('JJ', 879);
INSERT INTO user_integral (name, integral) VALUES('KK', 894);
INSERT INTO user_integral (name, integral) VALUES('LL', 1231);
INSERT INTO user_integral (name, integral) VALUES('MM', 478);
INSERT INTO user_integral (name, integral) VALUES('NN', 894);
复制代码
MySQL知识点:
可以使用@来定义变量,如:@a,这是我们定义一个变量
使用:=来给变量赋值,@a:=1,则@a的变量值为1
sql语句中,if(A,B,C)表示,如果A条件成立,那么执行B,否则执行C,如: @a := if(2>1,100,200)的结果是,a的值为100, 和Java中的三元表达式或if-else是一个意思
case...when...then语句(和java中的 switch-case-default 一个意思)
CASEWHEN expression THEN 操作1
WHEN expression THEN 操作2
.......
ELSE 操作n
END复制代码
从上往下执行,只要走了其中一个when,或者else,其他的就不走了,else当上面when都不走的时候,默认走else;
注意: case 后面带表达式,此时when 后面的则是该表达式可能的值
排名也有多种排名方式,如直接排名、分组排名,排名有间隔或排名无间隔等等
1、直接排序(普通排序)
直接根据积分做倒序排序,使用@ranks变量来标记用户的排名
explainSELECTname, integral , @ranks := @ranks + 1ASrankFROMuser_integral, (SELECT @ranks := 0) rORDERBYintegraldesc;
#排名结果
name|integral|rank|
----+--------+----+
FF | 3456| 1|
BB | 1235| 2|
LL | 1231| 3|
GG | 1000| 4|
KK | 894| 5|
NN | 894| 6|
EE | 879| 7|
JJ | 879| 8|
DD | 556| 9|
MM | 478| 10|
CC | 345| 11|
HH | 212| 12|
AA | 123| 13|
II | 111| 14|
复制代码
(SELECT @ranks := 0)的作用是:在同一个select语句中给变量ranks赋初始值。效果等同于,两个sql语句,第一个先赋值,第二个再select,
可以看到能够达到排序的效果,但是分数相同的时候排名依次往下了,这就不好了,实际开发中我们还可以再根据更新时间排序,相同分数的用户根据时间早的最排序,也就是正序排序,当然这是后话。
2、无间隔排序
实现分数相同,名次相同,排名无间隔
SELECTname, integral,
CASEWHEN@prevRank = integral THEN @curRankWHEN@prevRank := integral THEN @curRank := @curRank + 1
END AS rank
FROM user_integral,
(SELECT @curRank :=0, @prevRank := NULL) r
ORDER BY integral desc;
#排名结果
name|integral|rank|
----+--------+----+
FF | 3456|1 |
BB | 1235|2 |
LL | 1231|3 |
GG | 1000|4 |
KK | 894|5 |
NN | 894|5 |
EE | 879|6 |
JJ | 879|6 |
DD | 556|7 |
MM | 478|8 |
CC | 345|9 |
HH | 212|10 |
AA | 123|11 |
II | 111|12 |
复制代码
3、并列排名
并列排名,排名有间隔
SELECTname, integral, rankFROM
(SELECT name, integral,
@curRank := IF(@prevRank = integral, @curRank, @incRank) AS rank,
@incRank := @incRank + 1,
@prevRank := integral
FROM user_integral, (
SELECT @curRank :=0, @prevRank := NULL, @incRank := 1
) r
ORDER BY integral desc) s;
#排序结果
name|integral|rank|
----+--------+----+
FF | 3456|1 |
BB | 1235|2 |
LL | 1231|3 |
GG | 1000|4 |
KK | 894|5 |
NN | 894|5 |
EE | 879|7 |
JJ | 879|7 |
DD | 556|9 |
MM | 478|10 |
CC | 345|11 |
HH | 212|12 |
AA | 123|13 |
II | 111|14 |
复制代码
4、小结
以上MySQL的3种方式都能实现多积分的排名,而且在MySQL8.0之后可以使用ROW_NUMBER(),DENSE_RANK(),RANK() 使用3个函数实现上面3中排序,但是这3种方式也存在问题,当时数据量大的时候就很慢的,可以看一下他们执行计划:
直接排序
![](https://i-blog.csdnimg.cn/blog_migrate/ab0a3cfd4f3f5e8071100b5010729054.png)
无间隔排序
![](https://i-blog.csdnimg.cn/blog_migrate/b364b8e3eb749f46ac55ea0703992773.png)
并列排序
![](https://i-blog.csdnimg.cn/blog_migrate/4f62ff5ef4e6a7ae8a6f236a3141ce94.png)
可以看到3个SQL都是走全表查询的,创建索引也没有用,这个时候就要考虑性能问题
三、使用Redis实现排行榜
1、保存排名
Redis中有zset这个常用数据类型,zset和set很像,最大的不同就是zset是有序的,
可以将用户的分数作为 score 值,把用户id作为 value 值,通过对 score 排序就可以得出用户分排名
key:指定一个键名; score:分数值,用来描述 member,它是实现排序的关键; member:要添加的成员(元素)
Redi会自动根据score进行排序
当 key 不存在时,将会创建一个新的有序集合,并把分数/成员(score/member)添加到有序集合中;当 key 存在时,但 key 并非 zset 类型,此时就不能完成添加成员的操作,同时会返回一个错误提示。
注意:
1、在有序集合中,成员是唯一存在的,但是分数(score)却可以重复。有序集合的最大的成员数为 2^32 - 1 (大约 40 多亿个)。
2、score是有长度限制的,超过长度会报错
相同分数时,我们可以根据达到这个分数的时间做为分数的小数部分,因为要考虑score是有长度限制,可以用当前时间戳减去9999999999;拼接为小数再转成double类型;
import org.springframework.data.redis.core.RedisTemplate;
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 同步用户无限模式积分到redis
*
* @param accountId 用户
* @param integralNum 积分
*/
public void integralSyncRedis(Integer accountId, Integer integralNum) {
//同步数据到redis
LocalDateTime now = LocalDateTime.now(TimeZone.getTimeZone("America/Los_Angeles").toZoneId());
long second = 9999999999L - now.toInstant(ZoneOffset.of("-8")).toEpochMilli() / 1000;
String dou = integralNum + "." + second + "";
double newCount = Double.parseDouble(dou);
redisTemplate.opsForZSet().add("Planes_Battle_Integral", accountId, newCount);
}
复制代码
不用在意时间戳的时区,因为我们公司主要业务在国外,需要考虑时区;不在意的可以直接System.currentTimeMillis()获取时间戳;
结果:
![](https://i-blog.csdnimg.cn/blog_migrate/99f7c6cf3958234bef0f19a5e1014be7.png)
2、获取排名
获取排名就很方便了,我们可以直接通key获取用的排名:
//获取用户排序 accountId:保存时的用户id
Long accountRank = redisTemplate.opsForZSet().reverseRank("Planes_Battle_Integral", accountId);复制代码
也可以获取一定返回内的排名:
//获取前100名
Set<Object> reverseRange = redisTemplate.opsForZSet().reverseRange("Planes_Battle_Integral", 0, 99);
//获取前100名的用户id
List<Integer> accountIds = new ArrayList<>();
if (reverseRange != null && reverseRange.size() > 0) {
List<Object> reverseRangeList = new ArrayList<>(reverseRange);
List<Integer> accountIds = reverseRangeList.stream().map(o -> (Integer) o).collect(Collectors.toList());
}
复制代码
这个时候我们可以直接使用Redis中的排序的积分值,也可根据获取到的100名的用户id,去数据库查询分数,我是查询数据库分数的,Redis只帮我做排名;
3、zset常用方法
方法 | 作用 |
add(K key, V value, double score) | 向指定key中添加元素,按照score值由小到大进行排列( 集合中对应元素已存在,会被覆盖,包括score) |
add(K key, Set tuples) | 向指定key中添加元素,按照score值由小到大进行排列(集合中对应元素已存在,会被覆盖,包括score) |
incrementScore(K key, V v1, double delta) | 增加key对应的集合中元素v1的score值,并返回增加后的值( v1不存在,直接新增一个元素) |
score(K key, Object o) | 获取key对应集合中o元素的score值 |
size(K key) 或zCard(K key) | 获取集合的大小,size(K key)的底层调用的还是 zCard(K key) |
count(K key, double min, double max) | 获取指定score区间里的元素个数,包括min、max |
range(K key, long start, long end) | 获取指定下标之间的值((0,-1)就是获取全部) |
rangeByScore(K key, double min, double max) | 获取指定score区间的值 |
rangeByScore(K key, double min, double max, long offset, long count) | 获取指定score区间的值,然后从给定下标和给定长度获取最终值 |
rank(K key, Object o) | 获取指定元素在集合中的索引,索引从0开始 |
reverseRank(K key, Object o) | 获取倒序排列的索引值,索引从0开始 |
reverseRange(K key, long start, long end) | 逆序获取对应下标的元素 |
remove(K key, Object… values) | 移除集合中指定的值 |
removeRange(K key, long start, long end) | 移除指定下标的值 |
removeRangeByScore(K key, double min, double max) | 移除指定score区间内的值 |
作者:不瑶碧莲