<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          用 Redis 搞定游戲中的實(shí)時(shí)排行榜,附源碼!

          共 14012字,需瀏覽 29分鐘

           ·

          2021-07-19 14:07

          文末推薦一本好書
          來(lái)源:嘉興ing
          來(lái)源:segmentfault.com/a/1190000019139010


          1. 前言

          前段時(shí)間剛為項(xiàng)目(手游)實(shí)現(xiàn)了一個(gè)實(shí)時(shí)排行榜功能, 主要特性:
          • 實(shí)時(shí)全服排名
          • 可查詢單個(gè)玩家排名
          • 支持雙維排序
          數(shù)據(jù)量不大, 大致在 1W ~ 50W區(qū)間(開(kāi)服, 合服會(huì)導(dǎo)致單個(gè)服角色數(shù)越來(lái)越多).

          2. 排行榜分類

          按照排行主體類型劃分, 主要分為:
          • 角色
          • 軍團(tuán)(公會(huì))
          • 坦克
          該項(xiàng)目是個(gè)坦克手游, 大致情況是每個(gè)角色有N輛坦克, 坦克分為多種類型(輕型, 重型等), 玩家可加入一個(gè)軍團(tuán)(公會(huì)).
          具體又可以細(xì)分為:
          • 角色

          - 戰(zhàn)斗力排行榜(1. 戰(zhàn)斗 2.等級(jí))
          - 個(gè)人競(jìng)技場(chǎng)排行榜(1. 競(jìng)技場(chǎng)排名)
          - 通天塔排行榜(1.通天塔層數(shù) 2.通關(guān)時(shí)間)
          - 威望排行榜(1.威望值 2.等級(jí))
          • 軍團(tuán)(公會(huì))

          - 軍團(tuán)等級(jí)排行榜(1.軍團(tuán)等級(jí) 2.軍團(tuán)總戰(zhàn)斗力)
          • 坦克(1.坦克戰(zhàn)斗力 2.坦克等級(jí))

          - 中型
          - 重型
          - 反坦克炮
          - 自行火炮
          ↑ 括號(hào)內(nèi)為排序維度

          3. 思路

          基于實(shí)時(shí)性的考慮, 決定使用Redis來(lái)實(shí)現(xiàn)該排行榜.
          文章中用到的redis命令如有不清楚的, 可參照  Redis在線手冊(cè)  .
          需要解決如下問(wèn)題:
          1. 復(fù)合排序(2維)
          2. 排名數(shù)據(jù)的動(dòng)態(tài)更新
          3. 如何取排行榜

          4. 實(shí)現(xiàn) 復(fù)合排序

          基于Redis的排行榜主要使用的是Redis的 有序集合(SortedSet)來(lái)實(shí)現(xiàn)
          添加 成員-積分 的操作是通過(guò)Redis的zAdd操作
          ZADD key score member [[score member] [score member] ...]
          默認(rèn)情況下, 若score相同, 則按照 member 的字典順序排序.

          4.1 等級(jí)排行榜

          首先以等級(jí)排行榜(1. 等級(jí) 2.戰(zhàn)力)為例, 該排行榜要求同等級(jí)的玩家, 戰(zhàn)斗力大的排在前. 因此分?jǐn)?shù)可以定為:
          分?jǐn)?shù) = 等級(jí)*10000000000 + 戰(zhàn)斗力
          游戲中玩家等級(jí)范圍是1~100, 戰(zhàn)力范圍0~100000000.
          此處設(shè)計(jì)中為戰(zhàn)斗力保留的值范圍是 10位數(shù)值, 等級(jí)是 3位數(shù)值, 因此最大數(shù)值為 13位 .
          有序集合的score取值是是64位整數(shù)值或雙精度浮點(diǎn)數(shù), 最大表示值是 9223372036854775807, 即能完整表示 18位 數(shù)值,因此用于此處的 13位score 綽綽有余.

          4.2 通天塔排行榜

          另一個(gè)典型排行榜是 通天塔排行榜(1.層數(shù) 2.通關(guān)時(shí)間) , 該排行榜要求通過(guò)層數(shù)相同的, 通關(guān)時(shí)間較早的優(yōu)先.
          由于要求的是通關(guān)時(shí)間較早的優(yōu)先, 因此不能像之前那樣直接 分?jǐn)?shù)=層數(shù)*10^N+通關(guān)時(shí)間 .
          我們可以將通關(guān)時(shí)間轉(zhuǎn)換為一個(gè)相對(duì)時(shí)間, 即 分?jǐn)?shù)=層數(shù)*10^N + (基準(zhǔn)時(shí)間 - 通關(guān)時(shí)間)
          很明顯的, 通關(guān)時(shí)間越近(大), 則 基準(zhǔn)時(shí)間 - 通關(guān)時(shí)間 值越小, 符合該排行榜要求.
          基準(zhǔn)時(shí)間的選擇則隨意選擇了較遠(yuǎn)的一個(gè)時(shí)間 2050-01-01 00:00:00 , 對(duì)應(yīng)時(shí)間戳2524579200
          最終, 分?jǐn)?shù) = 層數(shù)_ 10^N + (2524579200 - 通過(guò)時(shí)間戳)述分?jǐn)?shù)公式中, N取10, 即保留10位數(shù)的相對(duì)時(shí)間.

          4.3 坦克排行榜

          坦克排行榜跟其他排行榜的區(qū)別在于, 有序集合中的 member 是一個(gè)復(fù)合id, 由 uid_tankId 組成.
          這點(diǎn)是需要注意的.

          5. 排名數(shù)據(jù)的動(dòng)態(tài)更新

          還是以等級(jí)排行榜為例
          游戲中展示的等級(jí)排行榜所需的數(shù)據(jù)包括(但不限于):
          • 角色名
          • Uid
          • 戰(zhàn)斗力
          • 頭像
          • 所屬公會(huì)名
          • VIP等級(jí)
          由于這些數(shù)據(jù)在游戲過(guò)程中是會(huì)動(dòng)態(tài)變更的, 因此此處不考慮將這些數(shù)據(jù)直接作為 member 存儲(chǔ)在有序集合中.
          用于存儲(chǔ)玩家等級(jí)排行榜有序集合如下

              -- s1:rank:user:lv ---------- zset --
              | 玩家id1    | score1
              | ...
              | 玩家idN    | scoreN
              -------------------------------------
          member為角色uid, score為復(fù)合積分
          使用hash存儲(chǔ)玩家的動(dòng)態(tài)數(shù)據(jù)(json)

              -- s1:rank:user:lv:item ------- string --
              | 玩家id1    | 玩家數(shù)據(jù)的json串
              | ...
              | 玩家idN    | 
              -----------------------------------------
          使用這種方案, 只需要在玩家創(chuàng)建角色時(shí), 將該角色添加到等級(jí)排行榜中, 后續(xù)則是當(dāng)玩家 等級(jí)戰(zhàn)斗力 發(fā)生變化時(shí)需實(shí)時(shí)更新 s1:rank:user:lv 該玩家的復(fù)合積分即可. 若玩家其他數(shù)據(jù)(用于排行榜顯示)有變化, 則也相應(yīng)地修改其在 s1:rank:user:lv:item 中的數(shù)據(jù)json串.

          6. 取排行榜

          依舊以等級(jí)排行榜為例.
          目的

              需要從 `s1:rank:user:lv` 中取出前100名玩家, 及其數(shù)據(jù).
              
          用到的Redis命令

              [`ZRANGE key start stop [WITHSCORES]`](http://redisdoc.com/sorted_set/zrange.html)
              時(shí)間復(fù)雜度: O(log(N)+M), N 為有序集的基數(shù),而 M 為結(jié)果集的基數(shù)。
              
          步驟
          1. zRange("s1:rank:user:lv", 0, 99) 獲取前100個(gè)玩家的uid
          2. hGet("s1:rank:user:lv:item", $uid) 逐個(gè)獲取前100個(gè)玩家的具體信息
          具體實(shí)現(xiàn)時(shí), 上面的步驟2是可以優(yōu)化的.
          分析
          • zRange時(shí)間復(fù)雜度是O(log(N)+M) , N 為有序集的基數(shù),而 M 為結(jié)果集的基數(shù)
          • hGet時(shí)間復(fù)雜度是 O(1)
          • 步驟2由于最多需要獲取100個(gè)玩家數(shù)據(jù), 因此需要執(zhí)行100次, 此處的執(zhí)行時(shí)間還得加上與redis通信的時(shí)間, 即使單次只要1MS, 最多也需要100MS.
          解決
          • 借助Redis的Pipeline, 整個(gè)過(guò)程可以降低到只與redis通信2次, 大大降低了所耗時(shí)間.
          以下示例為php代碼

              // $redis
              $redis->multi(Redis::PIPELINE);
              foreach ($uids as $uid) {
                  $redis->hGet($userDataKey$uid);
              }
              $resp = $redis->exec();    // 結(jié)果會(huì)一次性以數(shù)組形式返回
          Tip: Pipeline 與 Multi 模式的區(qū)別
          參考:  https://blog.csdn.net/weixin_...
          • Pipeline 管線化, 是在客戶端將命令緩沖, 因此可以將多條請(qǐng)求合并為一條發(fā)送給服務(wù)端. 但是 不保證原子性 !!!
          • Multi 事務(wù), 是在服務(wù)端將命令緩沖, 每個(gè)命令都會(huì)發(fā)起一次請(qǐng)求, 保證原子性 , 同時(shí)可配合 WATCH 實(shí)現(xiàn)事務(wù), 用途是不一樣的.

          7. Show The Code


              <?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;
                  }
              
                  /**
                   * 新增/更新單人排行數(shù)據(jù)
                   * @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個(gè)
                   * @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);
                  }
              }
          這就是一個(gè)排行榜最簡(jiǎn)單的實(shí)現(xiàn)了, 排行項(xiàng)的積分計(jì)算由外部自行處理.

          好書推薦


           《深入理解計(jì)算機(jī)系統(tǒng)


          理解計(jì)算機(jī)系統(tǒng)首首選書目, 10余萬(wàn)程序員的共同選擇。卡內(nèi)基-梅隆、北京大學(xué)、清華大學(xué)、上海交通大學(xué)等國(guó)內(nèi)外眾多知名高校選用指定教材。從程序員視角全面剖析的實(shí)現(xiàn)細(xì)節(jié),使讀者深刻理解程序的行為,將所有計(jì)算機(jī)系統(tǒng)的相關(guān)知識(shí)融會(huì)貫通。每位不只想一直做碼農(nóng)的IT從業(yè)者都該仔細(xì)閱讀!


          瀏覽 24
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美xxx久久 | 羽月希在线播放 | 欧美中文字幕免费在线观看 | 亚洲免费视频24 | 狼人视频在线地址123 |