[TOC]php
Last-Modified: 2019年6月4日18:18:37html
前段時間剛爲項目(手遊)實現了一個實時排行榜功能, 主要特性:redis
數據量不大, 大體在 1W ~ 50W區間(開服, 合服會致使單個服角色數愈來愈多).json
按照排行主體類型劃分, 主要分爲:數組
該項目是個坦克手遊, 大體狀況是每一個角色有N輛坦克, 坦克分爲多種類型(輕型, 重型等), 玩家可加入一個軍團(公會).
具體又能夠細分爲:優化
角色ui
- 等級排行榜(1. 等級 2.戰力) - 戰鬥力排行榜(1. 戰鬥 2.等級) - 我的競技場排行榜(1. 競技場排名) - 通天塔排行榜(1.通天塔層數 2.通關時間) - 威望排行榜(1.威望值 2.等級)
軍團(公會)this
- 軍團戰鬥力排行榜(1. 軍團總戰鬥力 2.軍團等級) - 軍團等級排行榜(1.軍團等級 2.軍團總戰鬥力)
坦克(1.坦克戰鬥力 2.坦克等級).net
- 輕型坦克戰鬥力排行榜 - 中型 - 重型 - 反坦克炮 - 自行火炮
↑ 括號內爲排序維度
基於實時性的考慮, 決定使用Redis來實現該排行榜.設計
文章中用到的redis命令若有不清楚的, 可參照 Redis在線手冊.
須要解決以下問題:
基於Redis的排行榜主要使用的是Redis的 有序集合(SortedSet)來實現
添加 成員-積分 的操做是經過Redis的zAdd操做
ZADD key score member [[score member] [score member] ...]
默認狀況下, 若score相同, 則按照 member 的字典順序排序.
首先以等級排行榜(1. 等級 2.戰力)爲例, 該排行榜要求同等級的玩家, 戰鬥力大的排在前. 所以分數能夠定爲:
分數 = 等級*10000000000 + 戰鬥力
遊戲中玩家等級範圍是1~100, 戰力範圍0~100000000.
此處設計中爲戰鬥力保留的值範圍是 10位數值, 等級是 3位數值, 所以最大數值爲 13位.
有序集合的score取值是是64位整數值或雙精度浮點數, 最大表示值是 9223372036854775807, 即能完整表示18位數值, 所以用於此處的 13位score 綽綽有餘.
另外一個典型排行榜是 通天塔排行榜(1.層數 2.通關時間), 該排行榜要求經過層數相同的, 通關時間較早的優先.
因爲要求的是通關時間較早的優先, 所以不能像以前那樣直接 分數=層數*10^N+通關時間.
咱們能夠將通關時間轉換爲一個相對時間, 即 分數=層數*10^N + (基準時間 - 通關時間)
很明顯的, 通關時間越近(大), 則 基準時間 - 通關時間 值越小, 符合該排行榜要求.
基準時間的選擇則隨意選擇了較遠的一個時間 2050-01-01 00:00:00, 對應時間戳2524579200
最終, *分數 = 層數10^N + (2524579200 - 經過時間戳)
上述分數公式中, N取10, 即保留10位數的相對時間.
坦克排行榜跟其餘排行榜的區別在於, 有序集合中的 member 是一個複合id, 由 uid_tankId 組成.
這點是須要注意的.
仍是以等級排行榜爲例
遊戲中展現的等級排行榜所需的數據包括(但不限於):
因爲這些數據在遊戲過程當中是會動態變動的, 所以此處不考慮將這些數據直接做爲 member 存儲在有序集合中.
用於存儲玩家等級排行榜有序集合以下
-- s1:rank:user:lv ---------- zset -- | 玩家id1 | score1 | ... | 玩家idN | scoreN -------------------------------------
member爲角色uid, score爲複合積分
使用hash存儲玩家的動態數據(json)
-- s1:rank:user:lv:item ------- string -- | 玩家id1 | 玩家數據的json串 | ... | 玩家idN | -----------------------------------------
使用這種方案, 只須要在玩家建立角色時, 將該角色添加到等級排行榜中, 後續則是當玩家 等級戰鬥力 發生變化時需實時更新s1:rank:user:lv
該玩家的複合積分便可. 若玩家其餘數據(用於排行榜顯示)有變化, 則也相應地修改其在 s1:rank:user:lv:item
中的數據json串.
依舊以等級排行榜爲例.
目的
須要從 `s1:rank:user:lv` 中取出前100名玩家, 及其數據.
用到的Redis命令
[`ZRANGE key start stop [WITHSCORES]`](http://redisdoc.com/sorted_set/zrange.html) 時間複雜度: O(log(N)+M), N 爲有序集的基數,而 M 爲結果集的基數。
步驟
zRange("s1:rank:user:lv", 0, 99)
獲取前100個玩家的uidhGet("s1:rank:user:lv:item", $uid)
逐個獲取前100個玩家的具體信息具體實現時, 上面的步驟2是能夠優化的.
分析
解決
如下示例爲php代碼
// $redis $redis->multi(Redis::PIPELINE); foreach ($uids as $uid) { $redis->hGet($userDataKey, $uid); } $resp = $redis->exec(); // 結果會一次性以數組形式返回
Tip: Pipeline 與 Multi 模式的區別
參考: https://blog.csdn.net/weixin_...
WATCH
實現事務, 用途是不同的.<?php class RankList { protected $rankKey; protected $rankItemKey; protected $sortFlag; protected $redis; public function __construct($redis, $rankKey, $rankItemKey, $sortFlag=SORT_DESC) { $this->redis = $redis; $this->rankKey = $rankKey; $this->rankItemKey = $rankItemKey; $this->sortFlag = SORT_DESC; } /** * @return Redis */ public function getRedis() { return $this->redis; } /** * @param Redis $redis */ public function setRedis($redis) { $this->redis = $redis; } /** * 新增/更新單人排行數據 * @param string|int $uid * @param null|double $score * @param null|string $rankItem */ public function updateScore($uid, $score=null, $rankItem=null) { if (is_null($score) && is_null($rankItem)) { return; } $redis = $this->getRedis()->multi(Redis::PIPELINE); if (!is_null($score)) { $redis->zAdd($this->rankKey, $score, $uid); } if (!is_null($rankItem)) { $redis->hSet($this->rankItemKey, $uid, $rankItem); } $redis->exec(); } /** * 獲取單人排行 * @param string|int $uid * @return array */ public function getRank($uid) { $redis = $this->getRedis()->multi(Redis::PIPELINE); if ($this->sortFlag == SORT_DESC) { $redis->zRevRank($this->rankKey, $uid); } else { $redis->zRank($this->rankKey, $uid); } $redis->hGet($this->rankItemKey, $uid); list($rank, $rankItem) = $redis->exec(); return [$rank===false ? -1 : $rank+1, $rankItem]; } /** * 移除單人 * @param $uid */ public function del($uid) { $redis = $this->getRedis()->multi(Redis::PIPELINE); $redis->zRem($this->rankKey, $uid); $redis->hDel($this->rankItemKey, $uid); $redis->exec(); } /** * 獲取排行榜前N個 * @param $topN * @param bool $withRankItem * @return array */ public function getList($topN, $withRankItem=false) { $redis = $this->getRedis(); if ($this->sortFlag === SORT_DESC) { $list = $redis->zRevRange($this->rankKey, 0, $topN); } else { $list = $redis->zRange($this->rankKey, 0, $topN); } $rankItems = []; if (!empty($list) && $withRankItem) { $redis->multi(Redis::PIPELINE); foreach ($list as $uid) { $redis->hGet($this->rankItemKey, $uid); } $rankItems = $redis->exec(); } return [$list, $rankItems]; } /** * 清除排行榜 */ public function flush() { $redis = $this->getRedis(); $redis->del($this->rankKey, $this->rankItemKey); } }
這就是一個排行榜最簡單的實現了, 排行項的積分計算由外部自行處理.