dreamweaver网站制作教程互联网营销师培训费用是多少
游戏中经常会有排行榜需求需要实现,例如常见的战力排行榜、积分排行榜等等。
排行榜一般会用到 Redis 来实现,原因是:
- Redis 基于内存操作,速度快
- Redis 提供了高效的有序集合 zset
例如创建一个名为 rank 的排行榜
# 为用户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 同步数据到内存。
下面是一个简单的例子,实现了获取排行榜信息和用户排名数据。
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); } }
}
开发中,上面的例子还存在不少问题:
- 因为 redis 操作比较耗时,所以一般都会放在异步线程中进行操作
- 缓存数据的更新不是原子的,一旦多个用户同时请求,可能会导致数据重复更新多次
- 相同的分数的用户的排名会按照用户名来排序
针对于问题 3,因为用户在相同分数的情况下, redis 只支持根据用户名的字典排序,并不支持自定义排序。但是这对玩家来说是不可接受的。一个解决办法让相同分数的玩家按照达成时间的判断,最先抵达的玩家排名最高。
我们可以使用(真实分数 + 时间戳倒数)作为排名分数,真实分数作为整数部分,时间戳倒数作为小数部分。
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); }
}
参考:
- Redis sorted sets | Redis
- Redis实现排行榜及相同积分按时间排序 - 知乎
- Redis 浮点数累计实现-腾讯云开发者社区-腾讯云