手游服务端框架之使用Redis实现跨服排行榜

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/littleschemer/article/details/78167445

实现跨服排行榜的常规方法

游戏里为了刺激玩家的攀比心理,经常有各种各样的排行榜。排行榜又可以分为本服排行榜以及跨服排行榜。

简单说来,本服排行榜上的记录来自本服的玩家,而跨服排行榜上的记录是来自所有服务器前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多万,如果超过这个值,那么就要重新考虑位数的划分或者排行因素了。为了易于拓展,生成一维分数的方法必须允许子类修改。

扫描二维码关注公众号,回复: 3362839 查看本文章

跨服排行榜的代码实现

父级接口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





猜你喜欢

转载自blog.csdn.net/littleschemer/article/details/78167445