在游戏的设计中,我们需要设计一个贸易利润和综合实力排行榜。在排行榜中,我们有这样的需求:
A: 排行榜4天更新一次,也就是,需要统计玩家4天内贸易利润和综合实力
B: 上榜的玩家有奖励发送。
C: 综合实力是统计全服玩家,如果玩家分库存储,需要遍历所有的玩家库
D:4天后,删除旧数据,更新原来的数据。
E:排行榜是全服共享的
F:上榜的玩家的信息是动态的,如,玩家的等级,玩家的国籍(游戏中,可以允许玩家修改国籍)
G: 我的排名显示,和排行榜中的排名显示,要一致。
我们在做最后一个需求的时候,出现过这样的问题,排名榜是全服共享的,而我的排名是我从数据库中实时读出来的,也就是说,当玩家A第一次查看排行榜的时候,此时会缓存排行榜的数据1个小时,如果玩家B在过来看排行榜的时候,其实,这个排行榜是玩家A缓存的。但是,“我的排名”是从数据库实时读出来的,如果玩家B在榜上,而且已经被缓存了,那么如果玩家B的排名发生变化,且缓存还没有失效,那么玩家的的排名就会和排行榜上的有所不同。
解决做法:
A 缓存我的排行榜和我的排名的数据。
B 读取我的排名的时候,如果他在排行榜上,优先读取排行榜中的排名
C 当更新排行榜的缓存的时候(即清空原来的缓存,然后缓存最新的信息),同时,清空 “我的排名” 的缓存。防止排行榜中的排名和我的实际排名不一致的问题
还有一个最大的问题:
排行榜的数据缓存1小时。每过4天后,重新统计排行榜的信息。也就是说,4天后,排行榜中的信息全部失效,重新统计,如果此时,被缓存的排行榜数据没有失效,那么,我读出来的排行榜的数据还是原来的数据。
怎么解决这个问题:
最初的想法是,直接删除原来缓存的数据,可是怎么删除呢,这个做法不可行。
最好的做法是,在缓存的key里面加入时间,如果时间过期,就不会读这个key对应的内容。
$cacheKey = 'Rank:' . $field . ':' . $year;
设计思路:
创建一个rank表,统计玩家4天内的贸易利润。
创建一个奖励配置rank_award表,给上榜的玩家发放奖励。
详细设计:
在设计rank表的时候,我们需要对“4”天这个时间段作限制。我们考虑的是,以某一个时间作为“标准”,计算4天后的时间,作为一个截止时间,也就是在这个截止时间内,统计玩家的获取的利润,到了这个时间点后,停止统计玩家获取的利润,同时,结算给玩家奖励,然后清除排行榜,同时重新统计玩家的信息。
游戏奖励表的设计:
award 的对应是: 1:100;2:200;这样的格式。
rank表的设计:
this_year 这个字段是一个截止时间的概念,在我们游戏中,有一个游戏年的概念,是4天作为一个游戏年,所以,我们正好使用这个游戏年,作为排行榜时间段。
这个this_year是这样被计算出来的:
游戏的开始元年是: startTime = 2013/1/1
当前游戏的天数: day = max(1, ceil(time() - strtotime(startTime))/24*60*60);
当前游戏的年数(4天作为一年): year = str_pad(ceil(day/4), 4,0 str_pad_left);
当前游戏的季节: season= (day%4) (0:春天 1:夏天 2:秋天 3:冬天)
这样做的目的,避免了每次插入用户的一条记录时,都需要做时间段的比对:
public static function incrYearlyRankStats($uid, $field, $step = 1) { $where = array( 'this_year' => $year, 'uid' => $uid, ); $result = $this->where($where)->increment($field, $step); // 更新影响行数为0 if (! $result) { // 如果记录确实不存在,则插入一条新记录 if (! $this->where($where)->fetchCount()) { // 执行插入 $setArr = array( 'this_year' => $year, 'uid' => $uid, $field => $step, ); $this->insert($setArr); } } return true; }
获取我的排名:
public function getMyRankNo($year, $uid, $field, $value = null) { $year = $year ?: $GLOBALS['_V_YEAR']; $cacheKey = 'Rank:' . $field . ':' . $year; // 若我在排行榜上,则从排行榜中读取我的排名 if ($list = F('Memcache')->get($cacheKey)) { foreach ($list as $rank) { if ($rank['uid'] == $uid) { return $rank['rank_no']; } } } // 获取排行榜的列表 $cacheKey = 'MyRankNoKey:' . $field . ':' . $year . ':' . $uid ; // 先从缓存中读取 if ($myRankNo = F('Memcache')->get($cacheKey)) { return $myRankNo; } $myRankNo = Dao('Share_RankYearly')->getMyRankNo($year, $uid, $field); // 保存到缓存中 F('Memcache')->set($cacheKey, $myRankNo, 3600); return $myRankNo; }
注意并列排名的问题:
/** * 对列表数组标记排位序号(支持并列排位) * * @param array $topList 已降序排好的列表数组 * @param string $scoreField 排名比较字段 * @return void */ public static function decorateRankNo(array $topList, $scoreField) { $rankNo = 1; $lastScore = 0; foreach ($topList as &$value) { if ($value[$scoreField] < $lastScore) { $rankNo++; } $lastScore = $value[$scoreField]; $value['rank_no'] = $rankNo; } return $topList; }
综合实力榜,涉及到玩家的分库问题,需要遍历所有的玩家库:
public static function getCombatPowerRank() { $list = array(); // 遍历所有用户分库,执行清理 for ($i = 1; $i <= DIST_USER_DB_NUM; $i++) { if ($distList = Dao('Dist_User')->setDs($i)->getCombatPowerTopUsers(self::RANK_LIMIT)) { $list = array_merge($list, $distList); // 使用array_merge()做数组的合并 } } // 重新排序 $list = Helper_Array::multiSort($list, array( 'combat_power' => SORT_DESC )); // 截取前N名 $list = array_slice($list, 0, self::RANK_LIMIT); // 使用array_slice()对数组进行切割 // 对列表数组标记排位序号(支持并列排位) return self::decorateRankNo($list, 'combat_power'); }