游戏中排行榜的后台实现

游戏中经常会有排行榜需求需要实现,例如常见的战力排行榜、积分排行榜等等。

排行榜一般会用到 Redis 来实现,原因是:

  1. Redis 基于内存操作,速度快
  2. Redis 提供了高效的有序集合 zset

例如创建一个名为 rank 的排行榜

bash 复制代码
# 为用户user1设置分数为1
> zadd rank 1 user1

# 获取排行榜中全部用户的排名和分数(分数顺序排序)
> zrange rank 0 -1 withscores
1) "user1"
2) "1"
3) "user2"
4) "2"
5) "user3"
6) "3"

# 获取排行榜中全部用户的排名和分数(分数倒序排序)
> zrevrange rank 0 -1 withscores
1) "user3"
2) "3"
3) "user2"
4) "2"
5) "user1"
6) "1"

# 获取排行榜中排名前2的用户的排名和分数(分数倒序排序)
> zrevrange rank 0 1 withscores
1) "user3"
2) "3"
3) "user2"
4) "2"

# 获取排行榜中用户user2的排名
> zrank rank user2
(integer) 1

纵然 redis 的速度很快,但是再加上网络请求的开销和单线程问题,也比不上应用内直接内存的速度,所以为了速度,一般会在游戏内缓存排行榜。获取排行榜时,优先从内存中获取,并定时从 redis 同步数据到内存。

下面是一个简单的例子,实现了获取排行榜信息和用户排名数据。

java 复制代码
public class RankTest {  
  
    @Data  
    @AllArgsConstructor    
    public static class UserRankInfo {  
        private long userID;  
        private int rank;  
        private double score;  
    }  
  
    /**  
     * 缓存的用户信息  
     */  
    private static final Map<Long, UserRankInfo> USER_RANK_INFO_MAP = new ConcurrentHashMap<>();  
    /**  
     * 上次同步时间  
     */  
    private static int LAST_SYNC_TIME = 0;  
    /**  
     * 每隔多长时间从redis同步一次  
     */  
    private static final int SYNC_EVERY_SECOND = 60 * 10;  
  
    /**  
     * 获取排行榜  
     */  
	public Collection<UserRankInfo> getRankList() {  
	    if ((int) (System.currentTimeMillis() / 1000) > LAST_SYNC_TIME + SYNC_EVERY_SECOND) {  
	        syncUserRankInfoMap();  
	    }  
	    return USER_RANK_INFO_MAP.values();  
	}
  
    private void syncUserRankInfoMap() {  
        try (Jedis jedis = new Jedis("127.0.0.1", 6379);) {  
            // 获取前50名的用户  
            Set<Tuple> tuples = jedis.zrevrangeWithScores("rank", 0, 49);  
            putUserRankInfoMap(tuples);  
            LAST_SYNC_TIME = (int) (System.currentTimeMillis() / 1000);  
        }  
    }  
  
    private void putUserRankInfoMap(Set<Tuple> tuples) {  
        USER_RANK_INFO_MAP.clear();  
        int rank = 0;  
        for (Tuple tuple : tuples) {  
            long userID = Long.parseLong(tuple.getElement());  
            UserRankInfo info = new UserRankInfo(userID, rank++, tuple.getScore());  
            USER_RANK_INFO_MAP.put(userID, info);  
        }  
    }  
  
    /**  
     * 获取用户排名信息  
     */  
    public UserRankInfo getUserRankInfo(long userID) {  
        if ((int) (System.currentTimeMillis() / 1000) > LAST_SYNC_TIME + SYNC_EVERY_SECOND) {  
            syncUserRankInfoMap();  
        }  
        return USER_RANK_INFO_MAP.get(userID);  
    }  

	/**  
	 * 设置用户分数  
	 */  
	public void setUserRankScore(long userID,double score){  
	    try (Jedis jedis = new Jedis("127.0.0.1", 6379);) {  
  
	        jedis.zadd("rank", score, String.valueOf(userID));  
	        // 获取前50名的用户  
	        Set<Tuple> tuples = jedis.zrevrangeWithScores("rank", 0, 49);  
	        putUserRankInfoMap(tuples);  
	        LAST_SYNC_TIME = (int) (System.currentTimeMillis() / 1000);  
	    }  
	}
}

开发中,上面的例子还存在不少问题:

  1. 因为 redis 操作比较耗时,所以一般都会放在异步线程中进行操作
  2. 缓存数据的更新不是原子的,一旦多个用户同时请求,可能会导致数据重复更新多次
  3. 相同的分数的用户的排名会按照用户名来排序

针对于问题 3,因为用户在相同分数的情况下, redis 只支持根据用户名的字典排序,并不支持自定义排序。但是这对玩家来说是不可接受的。一个解决办法让相同分数的玩家按照达成时间的判断,最先抵达的玩家排名最高。

我们可以使用(真实分数 + 时间戳倒数)作为排名分数,真实分数作为整数部分,时间戳倒数作为小数部分。

java 复制代码
public void setUserRankScore(long userID,int score){  
    try (Jedis jedis = new Jedis("127.0.0.1", 6379);) {  
        //因为毫秒时间戳最多有13位  
        double newScore=score+1000_000_000_000.0D/System.currentTimeMillis();  
        jedis.zadd("rank", newScore, String.valueOf(userID));  
        // 获取前50名的用户  
        Set<Tuple> tuples = jedis.zrevrangeWithScores("rank", 0, 49);  
        putUserRankInfoMap(tuples);  
        LAST_SYNC_TIME = (int) (System.currentTimeMillis() / 1000);  
    }  
}

参考:

  1. Redis sorted sets | Redis
  2. Redis实现排行榜及相同积分按时间排序 - 知乎
  3. Redis 浮点数累计实现-腾讯云开发者社区-腾讯云
相关推荐
_oP_i13 分钟前
Pinpoint 是一个开源的分布式追踪系统
java·分布式·开源
mmsx16 分钟前
android sqlite 数据库简单封装示例(java)
android·java·数据库
武子康42 分钟前
大数据-258 离线数仓 - Griffin架构 配置安装 Livy 架构设计 解压配置 Hadoop Hive
java·大数据·数据仓库·hive·hadoop·架构
豪宇刘2 小时前
MyBatis的面试题以及详细解答二
java·servlet·tomcat
秋恬意2 小时前
Mybatis能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别
java·数据库·mybatis
Ren_xixi2 小时前
redis和mysql的区别
数据库·redis·mysql
FF在路上3 小时前
Knife4j调试实体类传参扁平化模式修改:default-flat-param-object: true
java·开发语言
真的很上进3 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
众拾达人3 小时前
Android自动化测试实战 Java篇 主流工具 框架 脚本
android·java·开发语言
皓木.3 小时前
Mybatis-Plus
java·开发语言