亿级用户游戏排行榜设计方案

@Service

@Slf4j

public class RankingServiceImpl implements RankingService {

public CommonVO gameRanking(String userNo, String gameId, Integer leaderboardType, Long topN) {

RankingInfo rankingInfo = new RankingInfo();

try {

List 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 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> 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> rangeWithScores

= redisUtil.zRevrangeWithScore(bucketValue, 0L, topN - 1);

List<ZSetOperations.TypedTuple> 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 doTopNScoreList(Long topN) {

List userIdList = new ArrayList<>();

Set<ZSetOperations.TypedTuple> rangeWithScores

= redisUtil.zRevrangeWithScore(GameConstant.USER_SCORE_GENERAL_RANKING_TOPN, 0L, topN - 1);

List<ZSetOperations.TypedTuple> userScoreTuples = new ArrayList<>(rangeWithScores);

List 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 {

@Autowired

private 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[2i - 1], ARGV[2i])\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[2i - 1], ARGV[2i])\n" +

" end\n" +

" return 1\n" +

“end”;

private RedisScript redisDelAndZaddScript = new DefaultRedisScript<>(DELANDZADDSCRIPT, Long.class);

/**

  • 刪除及插入

  • @param key 键
    自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

由于篇幅限制,小编在此截出几张知识讲解的图解

P8级大佬整理在Github上45K+star手册,吃透消化,面试跳槽不心慌

P8级大佬整理在Github上45K+star手册,吃透消化,面试跳槽不心慌

P8级大佬整理在Github上45K+star手册,吃透消化,面试跳槽不心慌

P8级大佬整理在Github上45K+star手册,吃透消化,面试跳槽不心慌

P8级大佬整理在Github上45K+star手册,吃透消化,面试跳槽不心慌

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
ttps://img-community.csdnimg.cn/images/e5c14a7895254671a72faed303032d36.jpg" alt=“img” style=“zoom: 33%;” />

最后

由于篇幅限制,小编在此截出几张知识讲解的图解

[外链图片转存中…(img-bKkxzKQT-1713571728819)]

[外链图片转存中…(img-HScPjk94-1713571728820)]

[外链图片转存中…(img-C3y2Ub3t-1713571728822)]

[外链图片转存中…(img-YWacwQgJ-1713571728823)]

[外链图片转存中…(img-vVoTzYvk-1713571728825)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 28
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值