欢迎大家关注我的公众号【老周聊架构】,Java后端主流技术栈的原理、源码分析、架构以及各种互联网高并发、高性能、高可用的解决方案。

一、前言

不管是手游还是端游,貌似都离不开排行榜,没有排行榜的游戏是没有灵魂的游戏,因为排行榜可以让用户分泌多巴胺,这样日活才会上来,有了用户就有钱赚。产品想方设法的让用户留存,设计各种排行榜:个人段位排名、个人积分或金币排名、全球榜单实时排名。如果用户量少的话,直接用mysql一张表存储着用户跟某个段位或者积分,然后查的时候再从高到低order by排序下。当然用户量很少的话是可以的,但随着用户量猛增,达到千万、亿级的话,这个肯定行不通了。你可能说我加索引、再多的话分库分表总行了吧。思路是没错的,但这不是很好的方案,排行榜实时更新,亿级用户这io想象都怕。

接下来我就来说下我认为比较好的设计方案。Redis的sorted set数据结构,这简直就是为了排行榜而生的数据结构呀。使用Redis排名非常简单对于百万级别的用户不用耗费太多内存即可实现高效快速的排名,什么玩意,百万级别,题目不是亿级级别吗?客官稍安勿躁,这数据结构轻松应对百万是没问题的,与亿相差100倍的话,也会有性能瓶颈的。那我们有啥优化方案吗?有的,那就是针对sorted set进行分桶。好了,接下来我们就来看看如何设计。

二、设计方案

这种方案就能轻松应对亿级用户的游戏排行榜了,我这里是以积分排行榜来设计的,其它的类似。这里每个桶按照承载百万用户,然后分了100个桶,如果积分分布均匀的话,那就可以轻松应对了。当然你可能会说,有很多新手比如玩了几次这个游戏就没玩了,在[0,1000)区间这个桶内有很多用户。是的,这里我们实行之前,会有个预估。大一点的公司会有数据分析工程师来对游戏用户做个合理的预估,通过一系列高数、概率论的知识把这个分桶区间预估的尽可能准。小公司的话不需要分桶,不要过度设计了。当然也有小部分小公司也有这么大的体量的话,那只能自己预估了,然后后续动态的去调整。

对于查询top排名时,只要查看最高分区桶sorted set排名即可。

对于查询个体用户的排名,根据用户的积分判断看在哪个桶里,计算本桶该用户的排名与高于当前分桶范围的分桶用户相加得到相关用户的排名。

三、代码实现

1、GameRanking 游戏排行榜类

@Data
@Builder
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("game_ranking")
public class GameRanking {@TableId(value = "id", type = IdType.AUTO)private Integer id;/*** 用户昵称*/private String nickName;/*** 排行榜分数*/private Double leaderboardScore;/*** 排行榜类型*/private Integer leaderboardType;/*** 名次*/private Long ranking;/*** 用户称号*/private String grade;/*** 用户编号*/private String userNo;/*** 创建时间*/private LocalDateTime createTime;/*** 更新时间*/private LocalDateTime updateTime;
}

2、排行榜返回的RankingInfo类

@Data
public class RankingInfo {private List<GameRanking> scoreList;private GameRanking userSelf;
}

3、实现类

@Service
@Slf4j
public class RankingServiceImpl implements RankingService {public CommonVO gameRanking(String userNo, String gameId, Integer leaderboardType, Long topN) {RankingInfo rankingInfo = new RankingInfo();try {List<GameRanking> gameRankingList = doGameRanking(topN);GameRanking gameRankingSelf = doGameRankingSelf(userNo);rankingInfo.setScoreList(gameRankingList);rankingInfo.setUserSelf(gameRankingSelf);} catch (Exception e) {log.error("gameRanking exception", e);return CommonVO.error(CommonVOCode.SERVER_ERROR, "gameRanking exception");}return CommonVO.success(rankingInfo);}public List<GameRanking> doGameRanking(Long topN) {List<Map<String, Object>> dataMapList = new ArrayList<>();JSONArray jsonArray = JSONArray.parseArray(ConfigManager.get(GameConstant.USER_SCORE_RANKING_INTERVAL));int size = jsonArray.size();long totalNum = 0;for (int i = size - 1; i >= 0; i--) {JSONObject jsonObject = jsonArray.getJSONObject(i);String bucketName = jsonObject.getString("bucketName");long unitBucketNum = redisUtil.zCard(bucketName);totalNum += unitBucketNum;if (totalNum <= topN && unitBucketNum != 0) {List<Map<String,Object>> one = commonScoreList(bucketName, topN);dataMapList.addAll(one);} else if (totalNum >= topN) {List<Map<String,Object>> two = commonScoreList(bucketName, unitBucketNum);dataMapList.addAll(two);break;}}if (CollectionUtils.isEmpty(dataMapList)) {return Collections.emptyList();}Set<ZSetOperations.TypedTuple<String>> vals = dataMapList.stream().map(en -> new DefaultTypedTuple<>((String) en.get("userId"),(Double) en.get("score"))).collect(Collectors.toSet());// 计算排行榜前先将topN桶删除,防止之前进入桶的用户干扰redisUtil.delAndZaddExec(GameConstant.USER_SCORE_RANKING_TOPN, vals);return doTopNScoreList(topN);}public List<Map<String, Object>> commonScoreList(String bucketValue, Long topN) {Set<ZSetOperations.TypedTuple<String>> rangeWithScores= redisUtil.zRevrangeWithScore(bucketValue, 0L, topN - 1);List<ZSetOperations.TypedTuple<String>> userScoreTuples = new ArrayList<>(rangeWithScores);return userScoreTuples.stream().map(tuple -> {String userId = tuple.getValue();Double score = tuple.getScore();Map<String,Object> map = new HashMap<>();map.put("userId", userId);map.put("score", score);return map;}).collect(Collectors.toList());}public List<GameRanking> doTopNScoreList(Long topN) {List<String> userIdList = new ArrayList<>();Set<ZSetOperations.TypedTuple<String>> rangeWithScores= redisUtil.zRevrangeWithScore(GameConstant.USER_SCORE_GENERAL_RANKING_TOPN, 0L, topN - 1);List<ZSetOperations.TypedTuple<String>> userScoreTuples = new ArrayList<>(rangeWithScores);List<GameRanking> collect = userScoreTuples.stream().map(tuple -> {String userId = tuple.getValue();Double score = tuple.getScore();userIdList.add(userId);return GameRanking.builder().userNo(userId).leaderboardScore(score).ranking((long) (userScoreTuples.indexOf(tuple) + 1)).build();}).collect(Collectors.toList());List<Map<String,String>> nickNameList = gameRankingMapper.selectBatchByUserNo(userIdList);collect.stream().forEach(gameRanking -> {Map<String,String> entity = nickNameList.stream().filter(map -> map.get("userNo").equals(gameRanking.getUserNo())).findFirst().orElse(null);if (entity != null) {gameRanking.setNickName(entity.get("nickName"));}});// 增加段位功能long count = 0;for (int i = 0; i < collect.size(); i++) {count++;collect.get(i).setGrade(getUserGrade(count));}return collect;}public GameRanking doGameRankingSelf(String userNo) {Long selfRank = null;Double score = null;String nickName = null;try {GameRanking gameRanking = gameRankingMapper.selectOneByUserNo(userNo);if (Objects.isNull(gameRanking)) {nickName = getNickName(userNo);} else {nickName = gameRanking.getNickName();}score = gameRanking.getLeaderboardScore();// 看该用户是否在topN的排行里GameRanking rankingSelf = rankingSelfInTopN(userNo);if (rankingSelf != null) {return rankingSelf;}String bucketName = getBucketNameParseFromConfigCenter(score);Map<String, Object> map = Collections.synchronizedMap(new LinkedHashMap());Map<String, String> rankingIntervalMap = getRankingIntervalMapFromConfigCenter();// 桶位置比较for (Map.Entry<String, String> entry : rankingIntervalMap.entrySet()) {if (entry.getValue().compareTo(bucketName) >= 0) {Long perBucketSize = redisUtil.zCard(entry.getValue());map.put(entry.getValue(), perBucketSize);}}Long totalNum = 0L;for (Map.Entry<String, Object> entry : map.entrySet()) {if (Objects.isNull(entry.getValue())) {continue;}if (bucketName.equals(entry.getKey())) {// 自身桶的排名Long selfNum = redisUtil.zRevrank(bucketName, userNo) + 1;// 自身桶排名与自身桶前面的总人数相加totalNum += selfNum;} else {totalNum += Long.parseLong(entry.getValue().toString());}}selfRank = totalNum;} catch (NullPointerException e) {selfRank = null;score = null;log.warn("gameRanking userNo:{"+userNo+"} score is null", e);}return GameRanking.builder().userNo(userNo).leaderboardScore(score).nickName(nickName).ranking(selfRank).grade(getUserGrade(selfRank)).build();}public GameRanking rankingSelfInTopN(String userNo) {Double score = redisUtil.zScore(GameConstant.USER_SCORE_GENERAL_RANKING_TOPN, userNo);if (score == null) {return null;} else {Long rank = redisUtil.zRevrank(GameConstant.USER_SCORE_GENERAL_RANKING_TOPN, userNo);return GameRanking.builder().userNo(userNo).leaderboardScore(score).ranking(rank + 1).nickName(getNickName(userNo)).grade(getUserGrade(rank + 1)).build();}}public String getBucketNameParseFromConfigCenter(Double score) {JSONArray jsonArray = JSONArray.parseArray(ConfigManager.get(GameConstant.USER_SCORE_GENERAL_RANKING_INTERVAL));int size = jsonArray.size();boolean flag = false;for (int i = 0; i < size; i++) {JSONObject jsonObject = jsonArray.getJSONObject(i);String bucketInterval = jsonObject.getString("bucketInterval");String bucketName = jsonObject.getString("bucketName");String[] split = bucketInterval.substring(1, bucketInterval.length() - 1).split(",");if ((score >= Double.parseDouble(split[0]) && "+endless".equals(split[1])) ||(score >= Double.parseDouble(split[0]) && score < Double.parseDouble(split[1]))) {flag = true;} else {flag = false;}if (flag) {return bucketName;}}return "";}
}

4、原子性操作导致并发安全问题

redisUtil.delAndZaddExec(GameConstant.USER_SCORE_RANKING_TOPN, vals);

通过lua脚本保证原子一致性,解决并发安全问题。

public class RedisUtil {@Autowiredprivate StringRedisTemplate stringRedisTemplate;private static final String DELANDZADDSCRIPT ="if redis.call('zcard', KEYS[1]) > 0 then\n" +"   redis.call('del', KEYS[1])\n" +"   for i, v in pairs(ARGV) do\n" +"       if i > (table.getn(ARGV)) / 2 then\n" +"           break\n" +"       end\n" +"       redis.call('zadd', KEYS[1], ARGV[2*i - 1], ARGV[2*i])\n" +"   end\n" +"   return 1\n" +"else\n" +"   for i, v in pairs(ARGV) do\n" +"       if i > (table.getn(ARGV)) / 2 then\n" +"           break\n" +"       end\n" +"       redis.call('zadd',KEYS[1], ARGV[2*i - 1], ARGV[2*i])\n" +"   end\n" +"   return 1\n" +"end";private RedisScript<Long> redisDelAndZaddScript = new DefaultRedisScript<>(DELANDZADDSCRIPT, Long.class);/*** 刪除及插入* @param key 键* @param val 批量值*/public void delAndZaddExec(String key, Set<ZSetOperations.TypedTuple<String>> val) {if (StringUtils.isEmpty(key)) {throw new IllegalArgumentException();}Object[] args = new Object[val.size()*2];int i= 0;for (ZSetOperations.TypedTuple<String> it : val ) {args[2*i] = String.valueOf(it.getScore());args[2*i + 1] = it.getValue();i++;}stringRedisTemplate.execute(redisDelAndZaddScript, Collections.singletonList(key), args);}
}

其它非核心代码我就不贴了,至此,亿级用户游戏排行榜设计方案到此结束,希望对你有帮助,欢迎交流意见与看法。


欢迎大家关注我的公众号【老周聊架构】,Java后端主流技术栈的原理、源码分析、架构以及各种互联网高并发、高性能、高可用的解决方案。

亿级用户游戏排行榜设计方案相关推荐

  1. 亿级用户体量,千万级日活用户,《王者荣耀》高并发背后的故事!

    堪称中国最火爆的手机游戏"王者荣耀",拥有亿级用户体量,千万级日活用户,如何快速.低成本地保障业务突发?本文从该问题出发,论述了问题对应的解决方案,并对其效果做出总结. 作者:黎斌 ...

  2. 12月16日vivo开发者大会:揭秘 vivo 互联网服务亿级用户的技术架构演进之路

    PART ONE 摘要 2021 vivo开发者大会,将于2021年12月16日在线上直播.vivo 开发者大会是 vivo 一年一度面向科技.互联网行业及合作伙伴举办的大型会议. PART TWO ...

  3. 24 亿级用户超级 APP 背后的全技术大揭秘

    进入十亿级用户"APP 俱乐部",可以说是很多做 APP 的公司梦寐以求的目标. 2017 年,刚刚成立 2 年的茄子科技(海外 SHAREit Group)交出了一份亮眼的出海成 ...

  4. 亿级用户中心的设计与实践

    -     前言    - 用户中心是互联网最为基础的核心系统,随着业务和用户的增长,势必会带来不断的挑战.如何在亿级的情况下保证系统的高可用,高性能以及高安全,本文能够给你一套实践方案. 注1:本文 ...

  5. 10分钟搞懂:亿级用户的分布式数据存储解决方案

    分布式数据库和分布式存储是分布式系统中难度最大.挑战最大,也是最容易出问题的地方.互联网公司只有解决分布式数据存储的问题,才能支撑更多次亿级用户的涌入. 接下来,你将花费十分钟掌握以下三方面内容: 1 ...

  6. Flink+Hologres亿级用户实时UV精确去重最佳实践

    简介:Flink+Hologres亿级用户实时UV精确去重最佳实践 UV.PV计算,因为业务需求不同,通常会分为两种场景: 离线计算场景:以T+1为主,计算历史数据 实时计算场景:实时计算日常新增的数 ...

  7. 巧用 maxTimeMS 服务端超时,避免承载亿级用户的腾讯云数据库MongoDB服务雪崩

    腾讯云数据库MongoDB作为一款基于开源社区MongoDB版本的文档数据库产品,其承载着公司内外包括微信.看点.QQ音乐在内的亿级用户重量级APP产品.在某些场景的使用过程中,用户在客户端请求超时后 ...

  8. hive建立内部表映射hbase_快手 HBase 在千亿级用户特征数据分析中的应用与实践...

    分享嘉宾:陈杨 快手 编辑整理:Hoh Xil 内容来源:BigData NoSQL 12th Meetup 出品社区:DataFun 注:欢迎转载,转载请注明出处. 快手建设 HBase 差不多有2 ...

  9. Hologres如何支持亿级用户UV计算

    简介: 本文将介绍阿里云Hologres如何基于RoaringBitmap进行UV等高复杂度计算的方案,实现亿级用户万级标签亚秒级分析,帮助用户从Kylin平滑迁移到Hologres,实现更实时.开发 ...

最新文章

  1. SpringMvc之@RequestParam详解
  2. TensorFlow Dropout
  3. oracle精度说明符1~38_Oracle错误代码案例总结及解决方案
  4. VTK:小部件之TextWidget
  5. 手摸手教你使用vue-cli脚手架-详细步骤图文解析[vue入门]
  6. 腾讯云IoT全栈方案助力智慧交通基建,详解四大重点与两个案例
  7. 第五章 基元类型、引用类型、值类型 CLR学习第五课
  8. Android官方开发文档Training系列课程中文版:调用相机之简单摄像
  9. 美国只有两样东西比中国贵
  10. python读取写入文件_Python读取文件,写入文件,打开文件,删除文件,复制文件
  11. Zabbix(四):高级应用之--宏、网络发现测试实例
  12. How to support Specular-Glossiness in Three.js
  13. databinding 入门 加载本地图片和加载网络图片
  14. 血泪安装caffe教程
  15. 安卓QQ聊天记录导出、备份完全攻略
  16. SUBTYPE正规化数据类型
  17. python pip安装报错_基于windows下pip安装python模块时报错总结
  18. 研究生学历会是我职业生涯的遮羞布吗
  19. hdmi tv 的edid_HDMI之EDID分析
  20. 互亿天线短信接口php文档

热门文章

  1. GBase 8a集群v953版本自增列介绍
  2. java接口汽车品牌_根据品牌获取所有车型示例代码
  3. python命令数字雨_用Python实现黑客帝国代码雨效果(3种方式)
  4. CAD指令框找不到了怎么调出来?CAD指令框调出方法
  5. cadence -- allegro和ad9之间的转换
  6. Tkinter 教程之10个经典程序代码 数字时钟(教程含源码)
  7. JDBC---jar包下载与导入
  8. Linux下载工具photon,Photon v0.3.1 免费开源下载软件,替代迅雷的下载利器
  9. Navicat Premium 12.1.16.0 安装与激活
  10. (王道计算机组成原理)第五章中央处理器-第三节1:CPU内部单总线数据通路中数据的流动