手游服务端框架之使用Redis实现跨服排行榜
2017-10-06 22:14
489 查看
实现跨服排行榜的常规方法
游戏里为了刺激玩家的攀比心理,经常有各种各样的排行榜。排行榜又可以分为本服排行榜以及跨服排行榜。简单说来,本服排行榜上的记录来自本服的玩家,而跨服排行榜上的记录是来自所有服务器前N名玩家。通常,跨服排行榜含金量更大,奖励也更为丰富。从技术上而言,实现起来也更为麻烦。
典型地,实现跨服排行榜有一下几种思路。
取其中某个服务器作为中心服,用来收集各服排行榜数据并进行广播;
使用独立进程,例如web后台,向各个服务拉取排行榜数据;
利用Redis的SortedSet,由Redis自己实现排序
本文详细介绍如何使用Redis实现跨服排行榜
Redis集群的简单用法
Redis是一个Key-Value的缓存数据库。这里不做过多介绍。为了提高IO效率,最新的Redis支持集群服务。官方的Redis是不支持Windows环境,所以本文开发环境是在Linux Ubuntu上。Redis的java客户端实现是Jedis。下面的对RedisCluster的简单封装,包括对Redis的各种数据操作。public enum RedisCluster {
INSTANCE;
private JedisCluster cluster;
public void init() {
String url = "127.0.0.1:8001";
HashSet<HostAndPort> hostAndPorts = new HashSet<>();
String[] hostPort = url.split(":");
HostAndPort hostAndPort = new HostAndPort(hostPort[0], Integer.parseInt(hostPort[1]));
hostAndPorts.add(hostAndPort);
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(50);
poolConfig.setMinIdle(1);
poolConfig.setMaxIdle(10);
this.cluster = new JedisCluster(hostAndPorts, 2000, poolConfig);
}
public void destory() {
try {
cluster.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public Double zscore(String key, String member) {
try {
return cluster.zscore(key, member);
} catch (JedisException e) {
LoggerUtils.error("", e);
throw new JedisException(e);
}
}
public Set<Tuple> zrangeWithScores(String key, long start, long end) {
try {
return cluster.zrangeWithScores(key, start, end);
} catch (JedisException e) {
LoggerUtils.error("", e);
throw new JedisException(e);
}
}
public Set<Tuple> zrevrangeWithScores(String key, long start, long end) {
try {
return cluster.zrevrangeWithScores(key, start, end);
} catch (JedisException e) {
LoggerUtils.error("", e);
return new HashSet<>(0);
}
}
public Double zincrby(String key, double score, String member) {
try {
return cluster.zincrby(key, score, member);
} catch (JedisException e) {
LoggerUtils.error("", e);
return null;
}
}
public Long zrank(String key, String member) {
try {
return cluster.zrank(key, member);
} catch (JedisException e) {
LoggerUtils.error("", e);
return -1L;
}
}
public long hset(String key, String field, String value) {
try {
return cluster.hset(key, field, value);
} catch (JedisException e) {
LoggerUtils.error("", e);
}
return -1L;
}
public String hget(String key, String field) {
try {
return cluster.hget(key, field);
} catch (JedisException e) {
LoggerUtils.error("", e);
return null;
}
}
}
Redis实现跨服排行榜的技术要点
有了Redis的SortedSet,可以轻易实现角色id与分数的有序映射。而对于具体的排行榜记录,则可以利用Redis的hashmap数据结构进行存储。由于Redis的SortedSet的score类型为double,只有52位的整数精度。而业务上的排行榜经常需要多级排行。比如说,玩家等级排行榜需要实现等级高的玩家排在前面,当玩家等级相同,先达到高等级的需要排前面。
为了实现多级排行,我们需要将多维因素映射到一维因素。在52位精度,我们可以把低32位表示记录创建时间,高20位表示等级值。20位最大值为100多万,如果超过这个值,那么就要重新考虑位数的划分或者排行因素了。为了易于拓展,生成一维分数的方法必须允许子类修改。
跨服排行榜的代码实现
父级接口CrossRank.java代码排行榜抽象,包括一级排行指标,二级排行指标,生成时间,构建Redis数据key等抽象方法。public interface CrossRank {
int getRankType();
/**
* local server id
* @return
*/
int getServerId();
long getCreateTime() ;
long getPlayerId();
/**
* first level rank score
* @return
*/
int getScore() ;
/**
* second level rank score
* @return
*/
int getAid() ;
/** redis rank type key */
String buildRankKey();
/** redis rank record key */
String buildResultKey();
/** redis rank score */
long buildRankScore();
}AbstractCrossRank是CrossRank的骨架实现,尽可能提供更多方法的默认实现
second level rank score */ @Protobuf private int aid; /** 32位时间戳 */ protected long TIME_MAX_VALUE = 0xFFFFFFFFL; public AbstractCrossRank(long playerId, int score, int aid) { this.playerId = playerId; this.score = score; this.aid = aid; this.serverId = ServerConfig.getInstance().getServerId(); this.createTime = System.currentTimeMillis(); } public AbstractCrossRank(long playerId, int score) { this(playerId, score, 0); } public AbstractCrossRank() { } public int getServerId() { return serverId; } public long getPlayerId() { return this.playerId; } public long getCreateTime() { return createTime; } public int getScore() { return score; } public int getAid() { return aid; } @Override public String buildRankKey() { return "CrossRank_" + getRankType(); } @Override public String buildResultKey() { return getClass().getSimpleName() ; } @Override public double buildRankScore() { //default rank score // score | createtime // 20bits 32bits long timePart = (TIME_MAX_VALUE - getCreateTime()/1000) & TIME_MAX_VALUE; long result = (long)score << 32 | timePart; // System.err.println(( (long)score << 32)+"|"+timePart+"|"+result); return result; } @Override public String toString() { return "AbstractCrossRank [serverId=" + serverId + ", createTime=" + createTime + ", playerId=" + playerId + ", score=" + score + ", aid=" + aid + "]"; } }CrossLevelRank是一个示例实现,代表玩家等级数据/**
* cross server level rank
* @author kingston
*
*/
public class CrossLevelRank extends AbstractCrossRank {
// just for jprotobuf
public CrossLevelRank() {
}
public CrossLevelRank(long playerId, int score) {
super(playerId, score);
}
public int getRankType() {
return CrossRankKinds.LEVEL;
}
}CrossRankService是排行榜逻辑操作工具,提供排行榜数据的更新与查询
public class CrossRankService { private static CrossRankService instance; private RedisCluster cluster = RedisCluster.INSTANCE; private Map<Integer, Class<? extends AbstractCrossRank>> rank2Class = new HashMap<>(); public static CrossRankService getInstance() { if (instance != null) { return instance; } synchronized (CrossRankService.class) { if (instance == null) { instance = new CrossRankService(); instance.init(); } } return instance; } private void init() { rank2Class.put(CrossRankKinds.FIGHTING, CrossLevelRank.class); } public void addRank(CrossRank rank) { String key = rank.buildRankKey(); String member = buildRankMember(rank.getPlayerId()); double score = rank.buildRankScore(); cluster.zincrby(key, score, member); // add challenge result data. String data = RedisCodecHelper.serialize(rank); cluster.hset(rank.buildResultKey(), member, data); } private String buildRankMember(long playerId) { return String.valueOf(playerId); } public List<CrossRank> queryRank(int rankType, int start, int end) { List<CrossRank> ranks = new ArrayList<>(); Set<Tuple> tupleSet = cluster.zrevrangeWithScores("CrossRank_" + rankType, start , end ); Class<? extends AbstractCrossRank> rankClazz = rank2Class.get(rankType); for (Tuple record:tupleSet) { try{ String element = record.getElement(); AbstractCrossRank rankProto = rankClazz.newInstance(); String resultKey = rankProto.buildResultKey(); String data = cluster.hget(resultKey, element); CrossRank rank = unserialize(data, rankClazz); ranks.add(rank); }catch(Exception e) { e.printStackTrace(); } } return ranks; } public <T extends CrossRank> T unserialize(String rankData, Class<T> clazz) { return RedisCodecHelper.deserialize(rankData, clazz); } }测试代码,开启Redis集群服务后,执行RedisRankTest类单元测试
public class RedisRankTest { @Test public void test() { RedisCluster cluster = RedisCluster.INSTANCE; cluster.init(); cluster.clearAllData(); CrossRankService rankService = CrossRankService.getInstance(); final int N_RECORD = 10; for (int i=1;i<N_RECORD*2;i++) { rankService.addRank(new CrossLevelRank(i, 100+i)); } List<CrossRank> ranks = rankService.queryRank(CrossRankKinds.FIGHTING, 1, N_RECORD); for (CrossRank rank:ranks) { System.err.println(rank); } assertTrue(ranks.size() == N_RECORD); assertTrue(ranks.get(0).getScore() >= ranks.get(1).getScore()); } }
手游服务端开源框架系列完整的代码请移步github ->> jforgame
相关文章推荐
- 使用 Redis 实现排行榜功能
- SilverLight企业应用框架设计【五】客户端调用服务端(使用JSON传递数据,自己实现RESTful Web服务)
- 使用Redis实现用户积分排行榜的教程
- 使用 Redis 实现排行榜功能 (转载 https://segmentfault.com/a/1190000002694239)
- Mina框架使用---Android客户端的实现,断线重连,粘包处理(服务端非mina)
- 使用Redis实现用户积分排行榜的教程
- 手游服务端框架之使用事件驱动模型解决业务高耦合
- 使用 Redis 实现排行榜功能
- android iphone手机服务端接口(php实现,使用框架fat-free,解说例子blog)
- redis实战之使用redis实现排行榜
- 使用Redis实现用户积分排行榜的教程
- redis实战之使用redis实现排行榜
- 基于Python使用scrapy-redis框架实现分布式爬虫 注
- 使用 Redis 实现排行榜功能
- 手游服务端框架之使用Guava构建缓存系统
- 使用 IBM Rational Functional Tester 实现自动化框架: 数据驱动
- 服务使用CXF框架客户端使用Axis2框架的webservice实现方案
- 使用 LINQ To SQL 和实体框架实现灵活的数据访问
- SOA 数据访问--使用 LINQ To SQL 和实体框架实现灵活的数据访问
- 使用 nRoute 框架来实现基于 Silverlight 的桌面应用