您的位置:首页 > 数据库 > Redis

手游服务端框架之使用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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息