• 4. redis排名系统之C++实战操作对比MySQL


    一、MySQL实现方法

    假设我们要设计一款排名系统,那必然要涉及到两大类数据:武器数据和非武器的通用数据,它他通常有一个共用的属性:那就是主键唯一的,例如玩家的数字编号,通常在MySQL中是自增的无符号整数字段。

    非武器的通用数据可以理解为跟武器没有任何关联的数据,例如玩家ID,昵称,签名,注册时间,登录时间等等,在MySQL中就类似如下:

    武器数据这里每一个武器对应的种类就相当于MYSQL中的一个字段,大致类似如下:

    这个表单数据通常会只会增加并不会出现减少的现象,如果用MYSQL来做排名系统的话,当玩家数量越来越多成几何增长的时候,它会暴露出两个问题:

    1、效率过低:大量的select与update语句就可能会显得非常臃肿;

    2、不易扩展:如果需要实时增加新的武器数据类型(字段),可能会不方面

    这时候如果用redis来做数据存储就会显示格外合适,仿佛天生就是用来干这个事情的!

    二、用Redis实现方法

    1、存储前字符串编码转换

        通常不建议在REDIS中直接存储明文字符串,建议采用网页编码来存储,转换函数源码如下:

    1. // 十六进制字符表
    2. const UCHAR g_szHexTable[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
    3. //
    4. // 获取网页编码
    5. //
    6. int getUrlCode(const UCHAR *pszBuff, const int nSize, char *pszOutput, const int nMaxSize)
    7. {
    8. int i = 0;
    9. char *pszTemp = NULL;
    10. if (pszBuff == NULL || nSize == 0)
    11. return 0;
    12. if (pszOutput == NULL || nMaxSize == 0)
    13. return 0;
    14. memset(pszOutput, 0x00, nMaxSize);
    15. pszTemp = pszOutput;
    16. for (int i = 0; i < nSize; i++)
    17. {
    18. pszTemp[0] = '%';
    19. pszTemp[1] = '%';
    20. pszTemp[2] = g_szHexTable[(pszBuff[i] >> 4) & 0x0F];
    21. pszTemp[3] = g_szHexTable[pszBuff[i] & 0x0F];
    22. pszTemp += 4;
    23. }
    24. return nSize * 4;
    25. }
    26. // 测试函数
    27. void test_url_code()
    28. {
    29. UCHAR szInput[] = "我要打10个!";
    30. char szOutput[128] = { 0x00 };
    31. getUrlCode(szInput, strlen((char *)szInput), szOutput, 128);
    32. printf("%s\n", szOutput);
    33. }

    2、用户表操作

        我们可以采用Redis Hset 命令用于为哈希表中的字段赋值;如果哈希表不存在,一个新的哈希表被创建并进行 HSET 操作;如果字段已经存在于哈希表中,旧值将被覆;

    1. HSET KEY_NAME FIELD VALUE
    2. KEY_NAME: 键值
    3. FIELD: 字段
    4. VALUE: 值(字符传、数字、浮点)

    我们在KEY_NAME这里键值的时候,用一个组合方法来区分:

    1. // KEY_NAME = type : id
    2. #define STATS_ALL "a" // 总榜 a:1
    3. #define STATS_YEAR "y" // 年榜 y2022:1
    4. #define STATS_MONTH "m" // 月榜 m202205:1
    5. #define STATS_DAY "d" // 日榜 d20220501:1
    6. #define STATS_SESSION "s" // 季榜 s1_2022:1
    7. #define STATS_USER "user" // 用户信息 user:1

        那么SQL中修改的语句修改用户名的时候,像这样的:

    1. UPDATE dbuser SET name = 'aa' WHERE index = 1;
    2. UPDATE dbuser SET signature = '我要打10个!' WHERE index = 1;
    3. UPDATE dbuser SET regdate = '2023/10/1' WHERE index = 1;

        而在Redis中应该像这样:

    1. // 注意这里的KEY是组合出来的
    2. // 设置玩家ID=1的游戏昵称为"aa"
    3. HSET user:1 name %%61%%61
    4. // 设置玩家ID=1的游戏昵称为"我要打10个!"
    5. HSET user:1 signature %%CE%%D2%%D2%%AA%%B4%%F2%%31%%30%%B8%%F6%%A3%%A1
    6. // 设置玩家ID=1的注册时间为"2023/10/1"
    7. HSET user:1 regdate %%32%%30%%32%%33%%2F%%31%%30%%2F%%31
    8. // 设置玩家ID=1的经验值时间为100
    9. HSET user:1 exp 100
    10. // 上面redis命令也可以优化成下面的一句执行完
    11. HMSET user:1 name %%61%%61 signature %%CE%%D2%%D2%%AA%%B4%%F2%%31%%30%%B8%%F6%%A3%%A1 regdate %%32%%30%%32%%33%%2F%%31%%30%%2F%%31 exp 100

    C++代码片段如下:

    1. wsprintf(szCommand, /*HSET %s:%u %s %s*/XorStr<0xAB, 17, 0x72ACBFF2>("\xE3\xFF\xE8\xFA\x8F\x95\xC2\x88\x96\xC1\x95\x93\xC4\x98\x9C\xC9" + 0x72ACBFF2).s, STATS_USER, pInfo->m_nDbUid, g_szUserInfo[nPos], szValue);
    2. reply = (redisReply *)redisCommand(pRedis, szCommand);
    3. if (NULL == reply) goto STEP_END;
    4. if (REDIS_REPLY_ERROR == reply->type)goto STEP_END;
    5. bRet = TRUE;
    6. STEP_END:
    7. if (NULL != reply)
    8. {
    9. freeReplyObject(reply);
    10. reply = NULL;
    11. }

    3、武器数据操作

        在这方面数据操作就比较容易实现了,而且能随时扩充字段个数,不像SQL那样固定的个数;另外武器的字段的数值操作都是自加来累计,而不会产生添加或删除操作,比如当网关服务器提交一条玩家ID=1,武器=2,累计杀敌总数自加3的命令时,通常对应的SQL语句如下:

    1. // 修改总榜数据
    2. UPDATE dbweapon_all SET kill = kill + 3 where index = 1 AND wid = 2;
    3. // 修改年榜数据
    4. UPDATE dbweapon_2023 SET kill = kill + 3 where index = 1 AND wid = 2;
    5. // 修改赛季榜数据
    6. UPDATE dbweapon_s1_2023 SET kill = kill + 3 where index = 1 AND wid = 2;

        而在Redis中应该像这样:

    1. // HINCRBY命令原型
    2. // 参考地址:https://www.runoob.com/redis/hashes-hincrby.html
    3. HINCRBY KEY_NAME FIELD_NAME INCR_BY_NUMBER
    4. // redis执行3条命令
    5. HINCRBY a:1 1:kill 3
    6. HINCRBY y2023:1 1:kill 3
    7. HINCRBY s1_2023:1 1:kill 3
    8. 键值KEY_NAME注释:
    9. a:1 代表总榜
    10. y2023:1 外表年榜
    11. s1_2023:1 代表2023第一赛季榜
    12. 字段FIELD_NAME注释:
    13. 1:kill 武器编号为1,类型为kill
    14. 字段INCR_BY_NUMBER代表自加或自减数值

    4、MySQL修改积分查询排名

        通常来说需要写一个复杂的MySQL语句来计算玩家的积分,大致如下:

    1. // 修改积分
    2. UPDATE SET db_user exp = exp + 3.0 WHERE index = 1;
    3. // 查询积分
    4. SELECT exp FROM db_user WHERE index = 1;
    5. // 查询排名
    6. SELECT COUNT(*) + 1 AS rank
    7. FROM db_user
    8. WHERE exp >= (SELECT exp FROM db_user WHERE index = 1);

    而在redis中本身就有了现成的,通常在网关中处理武器数据时,会同时修改该榜单的积分值

    5、Redis Zincrby 修改积分

        Redis Zincrby 命令对有序集合中指定成员的分数加上增量 increment,可以通过传递一个负数值 increment ,让分数减去相应的值,比如 ZINCRBY key -5 member ,就是让 member 的 score 值减去 5 ;当 key 不存在,或分数不是 key 的成员时, ZINCRBY key increment member 等同于 ZADD key increment member;当 key 不是有序集类型时,返回一个错误;分数值可以是整数值或双精度浮点数。

    命令原型如下:

    1. ZINCRBY key increment member
    2. key 同上面的武器榜单
    3. increment 自增或自减的积分
    4. member 玩家id
    5. // 假设1号武器杀敌一次累计3分,那对应命令如下:
    6. ZINCRBY a:1 3.0 1
    7. ZINCRBY y2023:1 3.0 1
    8. ZINCRBY s1_2023:1 3.0 1

    C++代码片段如下:

    1. // 总榜记录
    2. wsprintf(szCommand, /*ZINCRBY %s %s %u*/XorStr<0x09, 17, 0x5A9256F5>("\x53\x43\x45\x4F\x5F\x4C\x56\x30\x34\x61\x33\x31\x66\x36\x32\x6D" + 0x5A9256F5).s, STATS_ALL, szScore, nDbUid);
    3. bRet = redisGetReply(m_pRedis, (void **)&reply);
    4. if (bRet != REDIS_OK)
    5. goto STEP_END;
    6. if (reply)
    7. {
    8. freeReplyObject(reply);
    9. reply = NULL;
    10. }

    6、Redis Zscore 查询积分

        Redis 有序集合(sorted set),Redis Zscore 命令返回有序集中,成员的分数值。 如果成员元素不是有序集 key 的成员,或 key 不存在,返回 nil 。

    命令原型如下:

    1. ZSCORE key member
    2. key 同上面的武器榜单
    3. member 玩家id
    4. // 查询不同榜单的积分命令:
    5. ZSCORE a:1 1
    6. ZSCORE y2023:1 1
    7. ZSCORE s1_2023:1 1

    C++操作代码片段如下:

    1. // 获取玩家积分(插件中只获取总榜记录)
    2. wsprintf(szCommand, /*ZSCORE %s %u*/XorStr<0x40, 13, 0xF87B9E21>("\x1A\x12\x01\x0C\x16\x00\x66\x62\x3B\x69\x6F\x3E" + 0xF87B9E21).s, STATS_ALL, nDbUid);
    3. reply = (redisReply *)redisCommand(pRedis, szCommand);
    4. if (NULL == reply) goto STEP_END;
    5. if (REDIS_REPLY_ERROR == reply->type)goto STEP_END;
    6. if (REDIS_REPLY_STRING == reply->type)
    7. {
    8. fScore = strtod(reply->str, NULL);
    9. }
    10. if (NULL != reply)
    11. {
    12. freeReplyObject(reply);
    13. reply = NULL;
    14. }

    7、Redis Zrevrank 查询排名

        Redis 有序集合(sorted set),Redis Zrevrank 命令返回有序集中成员的排名。其中有序集成员按分数值递减(从大到小)排序;排名以 0 为底,也就是说, 分数值最大的成员排名为 0 ;使用 ZRANK 命令可以获得成员按分数值递增(从小到大)排列的排名。

    命令原型如下:

    1. Zrevrank key member
    2. key 同上面的武器榜单
    3. member 玩家id
    4. // 查询不同榜单的排名命令:
    5. ZREVRANK a:1 1
    6. ZREVRANK y2023:1 1
    7. ZREVRANK s1_2023:1 1

    C++操作代码片段如下:

    1. // 获取在玩家排名(插件中只获取总榜记录)
    2. wsprintf(szCommand, /*ZREVRANK %s %u*/XorStr<0x01, 15, 0x9CCD219A>("\x5B\x50\x46\x52\x57\x47\x49\x43\x29\x2F\x78\x2C\x28\x7B" + 0x9CCD219A).s, STATS_ALL, nDbUid);
    3. reply = (redisReply *)redisCommand(pRedis, szCommand);
    4. if (NULL == reply) goto STEP_END;
    5. if (REDIS_REPLY_ERROR == reply->type)goto STEP_END;
    6. if (REDIS_REPLY_NIL == reply->type)
    7. {
    8. nRank = pServer->m_nMaxPlayers;
    9. bRet = TRUE;
    10. }
    11. if (REDIS_REPLY_INTEGER == reply->type)
    12. {
    13. nRank = (reply->integer & 0xFFFFFFFF) + 1;
    14. bRet = TRUE;
    15. }
    16. if (NULL != reply)
    17. {
    18. freeReplyObject(reply);
    19. reply = NULL;
    20. }

    三、总结避坑

    1、尽量不要使用固定字符串做KEY

    在编写排名的时候,一般武器数据的KEY中不会用到像"kill"这种字符类型,通常用一个数字代替方面扩展时,只需要修改enum宏即可,武器 + 数据类型 来拼接 %u:%u 的KEY_NAME,类似下面这种:

    1. // push数据类别 // 命令 字符 积分
    2. enum EnumPushType{
    3. EPT_UNKNOW = '@', // 未知 64 @ +0
    4. EPT_KILL, // 杀敌 65 A +2
    5. EPT_SHOT, // 射击 66 B +0
    6. EPT_HEADSHOT, // 爆头 67 C +1
    7. EPT_HIT, // 击中 68 D +0
    8. EPT_DAMAGE, // 伤害 69 E +0
    9. EPT_DEATH, // 死亡 70 F -2
    10. EPT_FIRSTKILL, // 首杀 71 G +1
    11. EPT_FIRSTDEATH, // 首死 72 H +0
    12. EPT_BOMB_DEFUSION, // 拆除C4 73 I +2
    13. EPT_BOMB_PLANTING, // 安装C4 74 J +2
    14. EPT_TIME_ONLINE, // 在线 75 K +0 每秒+0.002分(每小时7.2分)
    15. EPT_KILL_WORLD, // 摔死 76 L +0
    16. EPT_KILL_SELF, // 自杀次数 77 M +0
    17. EPT_MAX_PLAYER, // 最大玩家 78 N +0
    18. EPT_RANK, // 当前排名 79 O +0
    19. EPT_SCORE, // 当前积分 80 P +0
    20. // 身体伤害
    21. EPT_DMAGE_NONE, // 击中空枪 81 Q +0
    22. EPT_DMAGE_HEAD, // 击中头部 82 R +0
    23. EPT_DMAGE_CHEST, // 击中胸部 83 S +0
    24. EPT_DMAGE_STOMACH, // 击中胃部 84 T +0
    25. EPT_DMAGE_LEFTARM, // 击中左臂 85 U +0
    26. EPT_DMAGE_RIGHTARM, // 击中右臂 86 V +0
    27. EPT_DMAGE_LEFTEG, // 击中左脚 87 W +0
    28. EPT_DMAGE_RIGHTEG, // 击中右脚 88 X +0
    29. EPT_DMAGE_SHIELD, // 击中盾牌 89 Y +0
    30. // 武器+BKILL
    31. EPT_BKILL, // 被击杀 90 Z +0
    32. EPT_BHEAD, // 被爆头 91 [ +0
    33. // 击中次数
    34. EPT_HIT_NONE, // 击中空枪 92 \ +0
    35. EPT_HIT_HEAD, // 击中头部 93 ] +0
    36. EPT_HIT_CHEST, // 击中胸部 94 ^ +0
    37. EPT_HIT_STOMACH, // 击中胃部 95 _ +0
    38. EPT_HIT_LEFTARM, // 击中左臂 96 ` +0
    39. EPT_HIT_RIGHTARM, // 击中右臂 97 a +0
    40. EPT_HIT_LEFTEG, // 击中左脚 98 b +0
    41. EPT_HIT_RIGHTEG, // 击中右脚 99 c +0
    42. EPT_HIT_SHIELD, // 击中盾牌 100 d +0
    43. // 混战参数 add by MT 2023-09-30
    44. EPT_ROUND, // 总回合
    45. EPT_RWIN_T, // 回合:T杀完胜利
    46. EPT_RWIN_BOOM, // 回合:T爆炸胜利
    47. EPT_RWIN_CT, // 回合:CT杀完胜利
    48. EPT_RWIN_DEFUSE, // 回合:CT爆炸胜利
    49. EPT_RWIN_SAVED, // 回合:CT时间结束胜利
    50. EPT_RWIN_RESCUE, // 回合:CT解救人质胜利
    51. EPT_RWIN_NOT_RESCUE, // 回合:CT未解救人质胜利
    52. EPT_RWIN_TYPE1, // 回合:保留胜利1
    53. EPT_RWIN_TYPE2, // 回合:保留胜利2
    54. EPT_RLOSE, // 失败回合
    55. EPT_REVEN, // 平局回合
    56. // 比赛参数
    57. EPT_SESSION, // 总场次
    58. EPT_SWIN, // 胜利场次
    59. EPT_SLOSE, // 失败场次
    60. EPT_SEVEN, // 平局场次
    61. EPT_MVP, // 最佳次数
    62. EPT_RWS, // 每局贡献评分(伤害占比)
    63. // 每回合杀敌统计
    64. EPT_KILL_0, // 0K 酱油局
    65. EPT_KILL_1, // 1K
    66. EPT_KILL_2, // 2K
    67. EPT_KILL_3, // 3K
    68. EPT_KILL_4, // 3K
    69. EPT_KILL_5, // 5K
    70. EPT_KILL_6, // 6K
    71. EPT_KILL_7, // 7K
    72. EPT_KILL_8, // 8K
    73. EPT_KILL_9, // 9K
    74. EPT_KILL_10, // 10K
    75. EPT_KILL_11, // 11K
    76. EPT_KILL_12, // 12K
    77. EPT_KILL_13, // 13K
    78. EPT_KILL_14, // 14K
    79. EPT_KILL_15, // 15K
    80. EPT_KILL_16, // 16K
    81. // 残局统计
    82. EPT_1V1, // 1v1
    83. EPT_1V2, // 1v2
    84. EPT_1V3, // 1v3
    85. EPT_1V4, // 1v4
    86. EPT_1V5, // 1v5
    87. EPT_1V6, // 1v6
    88. EPT_1V7, // 1v7
    89. EPT_1V8, // 1v8
    90. EPT_1V9, // 1v9
    91. EPT_1V10, // 1v10
    92. EPT_1V11, // 1v11
    93. EPT_1V12, // 1v12
    94. EPT_1V13, // 1v13
    95. EPT_1V14, // 1v14
    96. EPT_1V15, // 1v15
    97. EPT_1V16, // 1v16
    98. // 残局信息
    99. EPT_1ROUND, // 残局场次
    100. EPT_1RWIN, // 残局胜利
    101. EPT_ASSIST, // 助攻次数
    102. EPT_ADR, // 场均实际伤害占比累计
    103. // 穿墙信息
    104. EPT_WALL_HIT, // 穿墙累计命中次数
    105. EPT_WALL_DAMAGE, // 穿墙累计射击伤害
    106. EPT_WALL_HEAD, // 穿墙累计爆头次数
    107. EPT_WALL_KILL, // 穿墙累计击杀次数
    108. EPT_BWALL_HIT, // 被穿墙累计命中次数
    109. EPT_BWALL_DAMAGE, // 被穿墙累计射击伤害
    110. EPT_BWALL_HEAD, // 被穿墙累计爆头次数
    111. EPT_BWALL_KILL, // 被穿墙累计击杀次数
    112. };

     这一个宏再对应一个double数组来计算得分,如此只需要一个这样的函数作为网关总接口即可:

    1. // 提交排名数据
    2. UINT redis_PushCommand(REDIS_SERVER *pServer, const UINT nDataType, const UINT nWeapon, UINT nDbUid, UINT nValue);

    2、尽量用事务方式读写

    最后注意一下用事务处理的方法操作,可以加快读写效率,普通的10倍以上,示例代码如下:

    1. // 新版采用事务方式读取,加快速度
    2. for (i = 0; i < MAX_WEAPONS; i++)
    3. {
    4. nWeapon = i + EPT_UNKNOW;
    5. for (j = 0; j < MAX_OPT_COUNT; j++)
    6. {
    7. wsprintf(szCommand, /*HGET %s:%u %u:%u*/XorStr<0x18, 17, 0xF6DE906E>("\x50\x5E\x5F\x4F\x3C\x38\x6D\x25\x05\x54\x02\x06\x51\x1F\x03\x52" + 0xF6DE906E).s, STATS_ALL, nDbUid, , EPT_UNKNOW + j, nWeapon);
    8. redisAppendCommand(pRedis, szCommand); // 事务读取
    9. }
    10. }
    11. // 遍历所有武器数据
    12. for (i = 0; i < MAX_WEAPONS; i++)
    13. {
    14. // 遍历所有参数
    15. for (j = 0; j < MAX_OPT_COUNT; j++)
    16. {
    17. nRet = redisGetReply(pRedis, (void **)&reply);
    18. if (nRet != REDIS_OK)
    19. continue;
    20. if (reply->type == REDIS_REPLY_STRING)
    21. {
    22. sData.m_nValue[i][j] = atoi(reply->str);
    23. }
    24. if (reply->type == REDIS_REPLY_INTEGER)
    25. {
    26. sData.m_nValue[i][j] = reply->integer & 0xFFFFFFFF;
    27. }
    28. }
    29. }

  • 相关阅读:
    Visio 无边框保存png和pdf文件
    HashMap 源码浅析
    用云手机运营TikTok有什么好处?
    vue的路由守卫中,在beforeRouteEnter中动态获取路由地址信息之to、from、next & vm的使用
    Spring Data中MongoDB文档中的唯一字段
    2022/07/29 入职健海JustFE团队,我学到了高效开发(年中总结)
    【直播回顾】昇思MindSpore易用性SIG2022上半年回顾总结
    函数的this指向,改变函数内部this指向3种方法
    一种编程语言,
    数据库安全-Redis&Hadoop&Mysql&未授权访问&RCE
  • 原文地址:https://blog.csdn.net/wangningyu/article/details/133756015