一、前言

年前公司有很多活动要进行定制开发,活动中有游戏可以玩,最后对每个人的游戏分数进行排行展示,最终根据排名发放奖品。乍一看需求确实很简单,直接order by score一下不就完事了?需求确实简单,但是有不少小坑,故在此记录一下。

二、需求

  • 排行榜展示前100名最佳分数排行榜
  • 如果当前登录人在100名之后,则展示内容有两项
    1. 前100名最佳分数排行榜
    2. 当前登录人排名以及前后两个用户的排名

数据库表设计

  • user_id:用户ID
  • user_nickname:用户昵称
  • score:分数
  • avatar_image_path:用户头像
  • user_type:用户类型

三、用数据库查询 - 先分组,后排序

因为用户可以玩多次游戏,所以表中同一个用户会有对此游戏分数记录。

  • 查寻当前登录人的最佳分数
SELECTuser_id,user_nickname,score,avatar_image_path,user_type
FROMgame_score_record
WHERE`user_id` = #{userId}
ORDER BY`score` DESCid ASC
LIMIT 1
  • 查寻当前登录人的最佳排名
SELECT count(*)FROM (SELECT id, user_id, max( score ) AS scoreFROM game_score_recordGROUP BY user_idHAVING score > #{myBestScore}-- 这里是判断若有和登录人相同分数的用户,前一名是先行记录的用户OR CASE WHEN score = #{myBestScore} THEN (score = #{myBestScore} AND id < #{myBestId}) else '' end) AS fo
  • 查询前100个用户最佳分数排行榜
SELECTuser_id,user_nickname,max( score ) AS score,avatar_image_path,user_type
FROMgame_score_record
GROUP BYuser_id
ORDER BYscore DESC,id ASC
LIMIT 100;--order by score是由分数从高到低进行排序;order by id是当相同分数时,最先记录的排在前面
  • 获取前一名以及后一名排名信息
-- 前一名信息
(SELECTid,user_id,max( score ) AS score,user_nickname,avatar_image_path,user_type
FROMgame_score_record
GROUP BYuser_id
HAVINGscore > #{myBestScore}-- 这里是判断若有和登录人相同分数的用户,前一名是先行记录的用户OR CASE WHEN score = #{myBestScore} THEN (score = #{myBestScore} AND id < #{myBestId}) else '' end
ORDER BYscore ASC,id DESC
LIMIT 1)UNION ALL-- 后一名信息
(SELECTid,user_id,max( score ) AS score,user_nickname,avatar_image_path,user_type
FROMgame_score_record
GROUP BYuser_id
HAVINGscore < #{myBestScore}OR CASE WHEN score = #{myBestScore} THEN (score = #{myBestScore} AND id > #{myBestId}) else '' end
ORDER BYscore DESC,id ASC
LIMIT 1)

数据库查询为了满足需求,sql之复杂,且效率极低。当我跑入百万级数据量时,上述所有查询耗时均超过了5s,很明显不可取的技术方案。

四、Redis强势介入

当发现数据量上去之后,SQL查寻非常之缓慢,便准备转为将数据存入Redis缓存中查询

温习一下Redis五大数据结构

1. string

  1. 介绍 :string 数据结构是简单的 key-value 类型。虽然 Redis 是用 C 语言写的,但是 Redis 并没有使用 C 的字符串表示,而是自己构建了一种 简单动态字符串(simple dynamic string,SDS)。相比于 C 的原生字符串,Redis 的 SDS 不光可以保存文本数据还可以保存二进制数据,并且获取字符串长度复杂度为 O(1)(C 字符串为 O(N)),除此之外,Redis 的 SDS API 是安全的,不会造成缓冲区溢出。
  2. 常用命令: set,get,strlen,exists,dect,incr,setex 等等。
  3. 应用场景 :一般常用在需要计数的场景,比如用户的访问次数、热点文章的点赞转发数量等等。

2. list

  1. 介绍 :list 即是 链表。链表是一种非常常见的数据结构,特点是易于数据元素的插入和删除并且且可以灵活调整链表长度,但是链表的随机访问困难。许多高级编程语言都内置了链表的实现比如 Java 中的 LinkedList,但是 C 语言并没有实现链表,所以 Redis 实现了自己的链表数据结构。Redis 的 list 的实现为一个 双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
  2. 常用命令: rpush,lpop,lpush,rpop,lrange、llen 等。
  3. 应用场景: 发布与订阅或者说消息队列、慢查询。

3. hash

  1. 介绍 :hash 类似于 JDK1.8 前的 HashMap,内部实现也差不多(数组 + 链表)。不过,Redis 的 hash 做了更多优化。另外,hash 是一个 string 类型的 field 和 value 的映射表,特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以 hash 数据结构来存储用户信息,商品信息等等。
  2. 常用命令: hset,hmset,hexists,hget,hgetall,hkeys,hvals 等。
  3. 应用场景: 系统中对象数据的存储。

4. set

  1. 介绍 : set 类似于 Java 中的 HashSet 。Redis 中的 set 类型是一种无序集合,集合中的元素没有先后顺序。当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。比如:你可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis 可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程。
  2. 常用命令: sadd,spop,smembers,sismember,scard,sinterstore,sunion 等。
  3. 应用场景: 需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景

5. sorted set

  1. 介绍: 和 set 相比,sorted set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列,还可以通过 score 的范围来获取元素的列表。有点像是 Java 中 HashMap 和 TreeSet 的结合体。
  2. 常用命令: zadd,zcard,zscore,zrange,zrevrange,zrem 等。
  3. 应用场景: 需要对数据根据某个权重进行排序的场景。比如在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息。
  • redis数据结构的结构图

采用Redis的sorted set数据结构存储排行榜数据

  • 初始化数据到redis中

// 获取BoundZSetOperations,后续对元素的增删改查都是操作该对象
String redisKey = "gameRank";
BoundZSetOperations<String, Object> bzo = redisTemplate.boundZSetOps(redisKey);Set<ZSetOperations.TypedTuple<Object>> tuples = new HashSet<>();
// 这里是有限制的,一次不能写入太多,我个人测试了一下,如果一次写入70W以上就会报错,具体临界值也不太清楚
for (int i = 0; i < 500000; i++) {// 构造函数中第一个参数为用户ID,第二个参数为分数ZSetOperations.TypedTuple<Object> objectTypedTuple = new DefaultTypedTuple<>(i + 1, (double) i);tuples.add(objectTypedTuple);tuples.add(objectTypedTuple);
}
bzo.add(tuples);

即使一次写入50w条数据到redis,执行耗时也在2s之内,非常之快。这里因为每次写入数量有限制,所以稍作修改,分两次执行即可达到百万级数据

  • redisTemplate相关API的Demo

Integer userId = 9527;
// 所有用户,正序排名
System.out.println("==============正序排名==============");
Set rankSet = bzo.range(0, -1);
rankSet.forEach(System.out::println);// 所有用户,降序排名
System.out.println("===============降序排名=============");
Set<Object> reverseRankSet = bzo.reverseRange(0, -1);
reverseRankSet.forEach(System.out::println);// 获取已有具体元素的降序排名, 如果找不到数据,rank值为null
System.out.println("===============获取已有具体元素的排名=============");
Long rank = bzo.reverseRank(userId);
System.out.println(userId + " 排名:" + (rank + 1));// rank值是从0起始,所以展示要 +1// 根据排名获取具体的元素,注意都是从0开始为第一个
System.out.println("==============获取名次区间在3, 5的元素集合==============");
Set<Object> reverseRange = bzo.reverseRange(3, 5);
reverseRange.forEach(System.out::println);System.out.println("==============根据分数区间值排序取值==============");
Set<Object> objects = bzo.rangeByScore(20, 20);
objects.forEach(System.out::println);System.out.println("==============获取分数区间的数量==============");
Long count = bzo.count(0, 20);
System.out.println(count);
  • 将每个用户的最高分数存入Redis

每次游戏结束后,得到用户当前游戏分数,与redis中该用户的分数进行比较,若redis中没有该用户数据,则直接存入redis;若当前用户游戏分数 > redis中的用户分数,则存入redis,此时会自动覆盖掉历史记录。

// 当前登录用户
Integer userId = 9527;
// 当前游戏分数
double currentScore = 985.0;
// 1. 将游戏记录存入数据库
// 略// 2. 获取操作redis sorted set的对象,将用户游戏最高分存入redis中
String redisKey = "gameRank";
BoundZSetOperations<String, Object> bzo = redisTemplate.boundZSetOps(redisKey);
// 2.1 获取用户在redis中存储的最高分数
Double bestScoreInRedis = bzo.score(userId);
// 2.2 若redis中没有该用户数据,则是第一次玩,将当前记录存入redis;若当前用户游戏分数 > redis中的用户分数,覆盖历史记录
if (Objects.isNull(bestScoreInRedis) || currentScore > bestScoreInRedis){// 2.3 直接添加,自动覆盖历史记录bzo.add(userId, currentScore);
}

这么写似乎没有什么问题,但redis内部机制导致业务上存在一个问题:遇到相同分数的用户数据,后记录的数据,在redis排序中却是排在前面的!

  • 相同分数的用户数据,后记录却排序在前的解决方式

由于我们的游戏分数都是整数的,redis中的分数是存入的double类型,所以决定在小数点后面做文章。
*** 整体思想:*** 数据的时间先后标识值,与一个提前定义的Integer.MAX_VALUE差值,添加到小数点后面。这样以来后添加的数据分数值肯定最大,但是与Integer.MAX_VALUE差值就是最小的,相同分数后添加的这就排在后面啦。

两种方案:

  1. 分数后面添加时间戳(如果同时在一个时间点操作的,当前运行时间的时间戳会有相同的情况,不如下面的方案)
  2. 游戏记录先入库后,获取新增记录的数据库主键ID,在分数后面添加(推荐,先后顺序交给数据库来定夺,肯定不会重复)

修正后:

// 当前登录用户
Integer userId = 9527;
// 当前游戏分数
double currentScore = 985.0;
// 1. 将游戏记录存入数据库
// 获取插入到数据库的主键ID,此处简写
int id = 980;// 2. 获取操作redis sorted set的对象,将用户游戏最高分存入redis中
String redisKey = "gameRank";
BoundZSetOperations<String, Object> bzo = redisTemplate.boundZSetOps(redisKey);
// 2.1 获取用户在redis中存储的最高分数
Double bestScoreInRedis = bzo.score(userId);
// 2.2 若redis中没有该用户数据,则是第一次玩,将当前记录存入redis;若当前用户游戏分数 > redis中的用户分数,覆盖历史记录
if (Objects.isNull(bestScoreInRedis) || currentScore > bestScoreInRedis){// 2.3 小数点前面为用户真实分数,后面则为用户游戏记录先后值与最大Integer的差值String redisScore = currentScore + "." + (Integer.MAX_VALUE - id);bzo.add(userId, redisScore);
}

所有问题已经解决!上线后无问题,查寻效率非常之高!看菜鸟教程中,redis里面存储的元素可以高到离谱,但没有真实测试过存储量,个人感觉随便满足日常开发了

五、相关链接

转载自:使用Redis高效率查寻游戏排行榜数据 - 飞GieGie - 博客园

相关不错的文章:如何用redis实现游戏排名更新_qq_16069927的博客-CSDN博客

用redis做游戏内的各种排行榜功能相关推荐

  1. 使用Redis的有序集合Zset实现排行榜功能

    游戏中存在各种各样的排行榜,比如玩家的等级排名.分数排名等.玩家在排行榜中的名次是其实力的象征,位于榜单前列的玩家在虚拟世界中拥有无尚荣耀,所以名次也就成了核心玩家的追求目标. 一个典型的游戏排行榜包 ...

  2. 使用Redis实现用户积分及TopN排行榜功能

    1 需求 添加积分 在用户签到的基础上添加用户积分,签到 1 天送 10 积分,连续签到 2 天送 20 积分,3 天送 30 积分,4 天以上均送 50 积分. 积分排行榜 2 表设计 利用MySQ ...

  3. 【C++游戏引擎Easy2D】想做游戏,这三个功能少不了(time+renderer+logger)

  4. 用 Redis 搞定游戏中的实时排行榜,附源码!

    原文:segmentfault.com/a/1190000019139010 1. 前言 前段时间刚为项目(手游)实现了一个实时排行榜功能, 主要特性: 实时全服排名 可查询单个玩家排名 支持双维排序 ...

  5. 通过redis实现游戏排行榜功能

    需求说明 水晶数量排行榜 英雄熟练度排版 只排前一万名,玩家只能看到前200名的数据和自己的名次 每个排行榜实时刷新,玩家可以延迟5分钟查看榜单数据,但是自己的名次需要尽可能实时查看 分值一样,则先达 ...

  6. php redis 搜索,php使用redis做热搜词排行榜

    最近,b2c商城项目中,要做热搜词排行榜,用sql数据库做:建立一张表,保存用户的搜索记录,然后count每一个搜索词,并按照count的次数降序排列.这样的话,对数据库的I/O太频繁了,而且在cou ...

  7. 微信小游戏制作大厅里的排行榜(跟游戏内的排行榜有区别)

    微信游戏大厅里的排行榜制作: var kvDataList = new Array(); var tempJson = {wxgame: {score: currentLevel, update_ti ...

  8. Python Django 基于 Redis做实时排行榜和排名

    目录 安装 django-redis 配置项目的 settings.py 文件 对 model 进行处理 使用 signals 的 pre_save 自动同步数据 使用 redis 的函数添加和获取数 ...

  9. redis+lua现实游戏中的一些常用功能

    为什么80%的码农都做不了架构师?>>>    游戏中一些常用的功能,仅仅使用redis提供的命令来实现,恐怕难度比较大.好在redis支持lua,能让一系列的操作变为原子操作,让这 ...

最新文章

  1. 读过本文才算真正了解Cassandra数据库
  2. onbeforeedit和onbeginedit数据不一致_Redis缓存与数据库产生不一致的问题该如何解决?...
  3. 服务器堡垒机登录方式
  4. 北京内推 | 微软亚洲研究院自然语言计算组招聘NLP研究型实习生
  5. EXEJ4 生成的java exe文件更换电脑后出现闪退情况解决办法
  6. java array使用_Java_ArrayLit详细用法
  7. 私有成员变量理解的补充
  8. C#使用NPOI导出excel设置单元格背景颜色
  9. 互联网大佬生存法则:如何防守周鸿祎?
  10. 奇计淫巧______bitset优化
  11. html字幕文本,HTML字幕
  12. 2021-04-30 AndroidStudio_3种按钮点击事件_小白龙抄作业
  13. 微信新表情真的太骚了!!
  14. python中使用splash如何挂代理?
  15. 每日一题【33】解析几何-椭圆的垂径定理与焦半径公式
  16. 【小程序源码】同名在线查询系统
  17. python爬虫之:IP代理池开源项目讲解
  18. IBMX3650M4服务器_安装内存_内存顺序
  19. 女孩子付钱用计算机,“让女生付钱太没面子了,你转账给我吧。”
  20. mt4软件怎么选对下载方式

热门文章

  1. 开关电源工作原理浅析
  2. 数据科学家定位和职业规划
  3. 微信小程序前端流程图(订票系统开发总结)
  4. uni-app H5使用flv.js直播拉流
  5. 数说故事与华为云签署全面合作协议,共同升级数字世界营销新体验
  6. allergro音乐术语什么意思_意大利语音乐表情术语发音 allegro assai
  7. 基于php音乐网站平台设计与实现
  8. 外汇/本币;结算/清算
  9. python 游戏辅助lol_用Python爬取英雄联盟(lol)全部皮肤
  10. 职业生涯之“一个萝卜一个坑”