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

一、背景

前段时间接了一个需求,需要做个飞机大战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知识点:

  1. 可以使用@来定义变量,如:@a,这是我们定义一个变量

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

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

  1. 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种方式也存在问题,当时数据量大的时候就很慢的,可以看一下他们执行计划:

直接排序

无间隔排序

并列排序

可以看到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区间内的值

作者:不瑶碧莲

链接:https://juejin.cn/post/7202961375554142266

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值