我正在参与「启航计划」
一、背景
前段时刻接了一个需求,需要做个飞机大战H5小游戏,游戏中要依据用户的积分进行排名,做个排行榜;
用户的积分保存在积分表中如下图,用户最新积分超越之前积分就更新用户的积分;
假如简单的来做,那么能够直接依据“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
一个意思)CASE WHEN expression THEN 操作1 WHEN expression THEN 操作2 ....... ELSE 操作n END
从上往下履行,只需走了其中一个when,或者else,其他的就不走了,else当上面when都不走的时分,默许走else;
注意: case 后面带表达式,此时when 后面的则是该表达式可能的值
排名也有多种排名办法,如直接排名、分组排名,排名有距离或排名无距离等等
1、直接排序(普通排序)
直接依据积分做倒序排序,运用@ranks
变量来标记用户的排名
explain SELECT name, integral , @ranks := @ranks + 1 AS rank
FROM user_integral, (SELECT @ranks := 0) 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| 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、无距离排序
完成分数相同,名次相同,排名无距离
SELECT name, integral,
CASE
WHEN @prevRank = integral THEN @curRank
WHEN @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、并排排名
并排排名,排名有距离
SELECT name, integral, rank FROM
(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种办法也存在问题,当时数据量大的时分就很慢的,能够看一下他们履行计划:
直接排序
无距离排序
并排排序
能够看到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()
获取时刻戳;
结果:
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区间内的值 |