基於redis排行榜的實戰總結


 
        

前言:
  之前寫過排行榜的設計和實現, 不同需求其背后的架構和設計模型也不一樣.
  平台差異, 有的立足於游戲平台, 為多個應用提供服務, 有的僅限於單個游戲.排名范圍差異, 有的面向全局排名, 有的只做朋友圈排名. 實時性差異, 離線統計有之, 實時排名更常見.
  不管如何, 本文將結合之前寫的網頁闖關游戲, 來具體闡述基於redis排行榜的實戰過程.

相關文章系列:
  之前寫過兩篇關於排行榜的文章, 不過那是針對游戲平台(類似微信, 手Q等)而言的. 每個用戶都有自己的排行榜, 不是全局性的.
  • 社交游戲的排行榜設計和實現(1)
  • 社交游戲的排行榜設計和實現(2)
針對游戲全局排行版的文章
基於redis的排行榜設計和實現

需求說明:
以闖關游戲為例, 其排行榜是基於玩家的闖關個數來進行排名的, 這是合乎合理. 但是若兩個玩家得分相同, 這種場景又該如何評定呢?
有一種思路是, 當得分相同時, 以玩家最近一關的破解時間來排定, 既鼓勵准確率, 又鼓勵速度. 換句話說, score(得分)為第一排序因素, time(破解時間)為第二排序因素.
然而, 如果采用redis的sorted set去實現, 只能設定單一的排序分值score. 這樣的話, 二級排序想借助redis, 似乎這條路行不通.
不要灰心, 夢想是有的, 萬一實現了呢? ^_^.
是的, 解決方案是有的, 先賣個關子, 且看下面分解. 同時也來分析下, 使用redis較之mysql的優勢在哪?

mysql方案:
玩家每闖過一關, 需要記錄其在該關的得分記錄. 另一方面玩家是存在重復闖關的行為, 因此在設計得分模型中, 該得分記錄也幫助去重.
闖關記錄數據模型
    CREATE TABLE IF NOT EXISTS `tb_game_record` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `userid` varchar(32) NOT NULL COMMENT '用戶標識',
      `gateid` int(11) NOT NULL COMMENT '關卡編號',
      `slove_time` bigint(20) NOT NULL COMMENT '解決時間點',
      PRIMARY KEY (`id`),
      UNIQUE KEY `userid` (`userid`,`gateid`)
    ) ENGINE=InnoDB  DEFAULT CHARSET=utf8; 
    注: userid表示用戶, gateid為關卡編號, slove_time為解決的時間點. userid+gateid是聯合唯一索引, 用於去重.
根據單純的依賴這個數據表的設計, top-n查詢會如何演化.
排行榜查詢類似於Top-N, 其SQL表達有些復雜, 為一個嵌套的子查詢.
1). 統計闖關數和最晚破關時間點
    SELECT userid, COUNT(gateid) AS score, MAX(slove_time) AS last_slove_time
    FROM tb_game_record
    GROUP BY userid
    2). 進行排序(按得分降序, 時間升序)
    SELECT useid, score, last_slove_time
    FROM (...)
    ORDER BY score DESC, last_slove_time ASC
 3). 整合的SQL+區間段
  SELECT userid, score, last_slove_time
    FROM (
        SELECT userid, COUNT(gateid) AS score, MAX(slove_time) AS last_slove_time
        FROM tb_game_record
        GROUP BY userid
    ) t
    ORDER BY score DESC, last_slove_time ASC
    LIMIT ?, ?
    總的來說, 還是比較順利的, 但是性能如何呢? 我們來做一下explain評估.

  

    子SQL使用到filesort, 這個是很耗性能, 但確實也無可奈何.
那有沒有改進的方案呢? 當然有, 為何不單獨引入一個得分表呢?
總得分記錄數據模型
  CREATE TABLE IF NOT EXISTS `tb_game_score` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `userid` varchar(32) NOT NULL,
      `score` int(11) NOT NULL,
      `last_slove_time` bigint(20) NOT NULL,
      UNIQUE KEY `id` (`id`),
      UNIQUE KEY `userid` (`userid`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    注: score為記錄的userid總得分.
每當玩家破解一關的時候, 就自動添加一, 雖然有所寫消耗, 但對於查詢top-n, 則幫了很大的忙.
TOP-N的查詢SQL演變為:
    SELECT userid, score, last_slove_time
    FROM tb_game_score
    ORDER BY score DESC, last_slove_time ASC
    如果使用explain進行sql分析:

  

    雖然也使用到了filesort, 但其數據規模卻比tb_game_record少了一個數量級.
當然它也引入了數據一致性的風險, 因此更新的時候需要做事務上的保護.

redis+mysql方案:
引入總得分記錄表, 在查詢上還是有一定性能損失的. redis被譽為內存數據結構服務器, 能否代替mysql+cache的功能呢?
至少在排行榜的功能上, 其數據結構sorted set是完全可以滿足要求的. 其可以代替得分記錄表, ^_^.
當然其難點在於二級排序的模型抽象, sorted set只支持一級排序(sorted set的score域為double類型), 所以問題就演變為能否構建一個映射函數, 把二級排序映射為一級排序(double域).
幸好在排行榜的需求上, 二級排序(score, time)是可以映射為一級排序的(sorted set的score)域.
可以簡單設定:
score(得分)+time(9999999999-unix的紀元秒, 且固定長度)
    注: unix的紀元秒, 在可預見的將來, 時間長度都是固定長度的, 且取負. score在前, time在后.
比如玩家A的得分為10, 最后闖關的關卡時間為2016/3/30 17:35:47, 則時間戳為:1459330547. 最終為:8540669452=9999999999 - 1459330547.
最后的sorted set的score得分值為: 108540669452.
    這樣就能完美的到達初期設定的二級排序的排行榜需求了.
映射函數設計注意點
這個其實很重要, 因為sorted set的score是double域, 其表達的精度其實是有所限制的. 如果超過這個精度限度, 那么無論幾級排序都是沒有意義的.
Double 域的表示
    1bit(符號位)
    11bits(指數位)
    52bits(尾數位)

    value of floating-point = significand x base ^ exponent , with sign
    (浮點) 數值 =      尾數    ×    底數 ^ 指數,(附加正負號)
    而2^52, 2^52 = 4503599627370496,一共16位,理論上, double的絕對精度為15位.
在映射函數中, 切記15位的上限限定. 之前的設定排行榜的排序映射, 總共為12位(2位游戲得分值, 10位unix紀元秒數), 這是滿足要求的.

總結:
網上對redis sorted set用於排行榜的文章很多, 但真正的案列解說並不多. 可能這種多級排序在應用中, 更常見.
 
        
公眾號&游戲站點:
  個人微信公眾號: 木目的H5游戲世界

  

  個人游戲作品集站點(尚在建設中...): www.mmxfgame.com,  也可直接ip訪問http://120.26.221.54/.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM