我正在参与「启航计划」

一、背景

前段时刻接了一个需求,需要做个飞机大战H5小游戏,游戏中要依据用户的积分进行排名,做个排行榜;

飞机大战-实现排行榜的2种方式

用户的积分保存在积分表中如下图,用户最新积分超越之前积分就更新用户的积分;

飞机大战-实现排行榜的2种方式

飞机大战-实现排行榜的2种方式

假如简单的来做,那么能够直接依据“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知识点:

  1. 能够运用@来定义变量,如:@a,这是咱们定义一个变量

  2. 运用:=来给变量赋值,@a:=1,则@a的变量值为1

  3. sql句子中,if(A,B,C)表示,假如A条件建立,那么履行B,不然履行C,如: @a := if(2>1,100,200)的结果是,a的值为100, 和Java中的三元表达式或if-else是一个意思

  4. 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种办法也存在问题,当时数据量大的时分就很慢的,能够看一下他们履行计划:

直接排序

飞机大战-实现排行榜的2种方式

无距离排序

飞机大战-实现排行榜的2种方式

并排排序

飞机大战-实现排行榜的2种方式

能够看到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种方式

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区间内的值