• 基于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/.
  • 相关阅读:
    Appium教程
    ES6对象类型判断
    MyBatisPlus的时间段和模糊查询
    一个div中多个元素垂直居中的一种解决办法
    @JsonFormat与@DateTimeFormat注解的使用
    java日期类型对象通过mybatis向数据库中的存取
    Vue.js单向绑定和双向绑定实例分析
    Maven的使用
    如何将本地的项目提交到码云的远程仓库
    Linux CentOS7 的安装
  • 原文地址:https://www.cnblogs.com/mumuxinfei/p/5337329.html
Copyright © 2020-2023  润新知