4. (C++) Use redis or MySQL to implement a shooting game ranking system

1. MySQL implementation method

Suppose we want to design a ranking system, which must involve two major types of data: weapon data and non-weapon general data. They usually have a common attribute: that is, the primary key is unique, such as the player's numerical number, usually In MySQL, it is an auto-incrementing unsigned integer field.

General data of non-weapons: can be understood as data not related to weapons, such as player ID, nickname, signature, registration time , login time, etc., it is similar to the following in MySQL:

Weapon data:The type corresponding to each weapon here is equivalent to a field in MYSQL, roughly similar to the following:

This form data usually only increases and does not decrease. If MYSQL is used as a ranking system, when the number of players increases exponentially, it will expose two problems:

1. The efficiency is too low: a large number of select and update statements may appear very bloated;

2. Not easy to expand: If you need to add new weapon data types (fields) in real time, it may be inconvenient

At this time, it will be particularly suitable to use redis for data storage, as if it was born to do this!

2. Implementation method using Redis

1. String encoding conversion before storage

    It is usually not recommended to store plaintext strings directly in REDIS. It is recommended to use web page encoding to store them. The source code of the conversion function is as follows:

// 十六进制字符表
const UCHAR			g_szHexTable[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };

//
// 获取网页编码
//
int getUrlCode(const UCHAR *pszBuff, const int nSize, char *pszOutput, const int nMaxSize)
{
	int		i = 0;
	char	*pszTemp = NULL;

	if (pszBuff == NULL || nSize == 0)
		return 0;

	if (pszOutput == NULL || nMaxSize == 0)
		return 0;

	memset(pszOutput, 0x00, nMaxSize);
	pszTemp = pszOutput;

	for (int i = 0; i < nSize; i++)
	{
		pszTemp[0] = '%';
		pszTemp[1] = '%';
		pszTemp[2] = g_szHexTable[(pszBuff[i] >> 4) & 0x0F];
		pszTemp[3] = g_szHexTable[pszBuff[i] & 0x0F];
		pszTemp += 4;
	}

	return nSize * 4;
}

// 测试函数
void test_url_code()
{
	UCHAR	szInput[] = "我要打10个!";
	char	szOutput[128] = { 0x00 };

	getUrlCode(szInput, strlen((char *)szInput), szOutput, 128);
	printf("%s\n", szOutput);
}

2. User table operations

    We can use the Redis Hset command to assign values ​​to fields in the hash table; if the hash table does not exist, a new hash table is created and the HSET operation is performed; if the field already exists in the hash table, the old value will covered; covered

HSET KEY_NAME FIELD VALUE 

KEY_NAME: 键值
FIELD:    字段
VALUE:    值(字符传、数字、浮点)

When we use a combination method to distinguish the key value of KEY_NAME:

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

    Then when the modified statement in SQL modifies the user name, it looks like this:

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

    And in Redis it should be like this:

// 注意这里的KEY是组合出来的

// 设置玩家ID=1的游戏昵称为"aa"
HSET user:1 name %%61%%61

// 设置玩家ID=1的游戏昵称为"我要打10个!"
HSET user:1 signature %%CE%%D2%%D2%%AA%%B4%%F2%%31%%30%%B8%%F6%%A3%%A1

// 设置玩家ID=1的注册时间为"2023/10/1"
HSET user:1 regdate %%32%%30%%32%%33%%2F%%31%%30%%2F%%31

// 设置玩家ID=1的经验值时间为100
HSET user:1 exp 100


// 上面redis命令也可以优化成下面的一句执行完
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

The C++ code snippet is as follows:

	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);

	reply = (redisReply *)redisCommand(pRedis, szCommand);
	if (NULL == reply) goto STEP_END;
	if (REDIS_REPLY_ERROR == reply->type)goto STEP_END;
	bRet = TRUE;

STEP_END:
	if (NULL != reply)
	{
		freeReplyObject(reply);
		reply = NULL;
	}

3. Weapon data operation

    In this regard, data operations are relatively easy to implement, and the number of fields can be expanded at any time, unlike the fixed number like SQL; in addition, the numerical operations on the fields of weapons are self-added and cumulative, without adding or deleting operations. , for example, when the gateway server submits a command that player ID=1, weapon=2, and the cumulative number of enemies killed increases by 3, the corresponding SQL statement is usually as follows:

// 修改总榜数据
UPDATE dbweapon_all SET kill = kill + 3 where index = 1 AND wid = 2;

// 修改年榜数据
UPDATE dbweapon_2023 SET kill = kill + 3 where index = 1 AND wid = 2;

// 修改赛季榜数据
UPDATE dbweapon_s1_2023 SET kill = kill + 3 where index = 1 AND wid = 2;

    And in Redis it should be like this:

// HINCRBY命令原型
// 参考地址:https://www.runoob.com/redis/hashes-hincrby.html
HINCRBY KEY_NAME FIELD_NAME INCR_BY_NUMBER

// redis执行3条命令 
HINCRBY a:1 1:kill 3
HINCRBY y2023:1 1:kill 3
HINCRBY s1_2023:1 1:kill 3

键值KEY_NAME注释:
a:1       代表总榜
y2023:1   外表年榜
s1_2023:1 代表2023第一赛季榜

字段FIELD_NAME注释:
1:kill    武器编号为1,类型为kill

字段INCR_BY_NUMBER代表自加或自减数值

4. MySQL modifies points query ranking

    Generally speaking, you need to write a complex MySQL statement to calculate the player's points, which is roughly as follows:

// 修改积分
UPDATE SET db_user exp = exp + 3.0 WHERE index = 1;

// 查询积分
SELECT exp FROM db_user WHERE index = 1;

// 查询排名
SELECT COUNT(*) + 1 AS rank 
FROM db_user 
WHERE exp >= (SELECT exp FROM db_user WHERE index = 1);

There is a ready-made one in redis itself. Usually when the weapon data is processed in the gateway, the points value of the list will be modified at the same time.

5. Redis Zincrby modify points

    The Redis Zincrby command adds increment to the score of the specified member in the ordered set. You can pass a negative value increment to subtract the corresponding value from the score. For example, ZINCRBY key -5 member means subtracting 5 from the score value of the member. ; When the key does not exist, or the score is not a member of the key, the ZINCRBY key increment member is equivalent to the ZADD key increment member; when the key is not an ordered set type, an error is returned; the score value can be an integer value or a double-precision floating point number.

The command prototype is as follows:

ZINCRBY key increment member

key        同上面的武器榜单
increment  自增或自减的积分
member     玩家id


// 假设1号武器杀敌一次累计3分,那对应命令如下:
ZINCRBY a:1 3.0 1
ZINCRBY y2023:1 3.0 1
ZINCRBY s1_2023:1 3.0 1

The C++ code snippet is as follows:

	// 总榜记录
	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);

	bRet = redisGetReply(m_pRedis, (void **)&reply);
	if (bRet != REDIS_OK)
		goto STEP_END;

	if (reply)
	{
		freeReplyObject(reply);
		reply = NULL;
	}

6. Redis Zscore query points

    Redis ordered set (sorted set), the Redis Zscore command returns the score value of the member in the ordered set. If the member element is not a member of the sorted set key, or key does not exist, nil is returned.

The command prototype is as follows:

ZSCORE key member

key        同上面的武器榜单
member     玩家id


// 查询不同榜单的积分命令:
ZSCORE a:1 1
ZSCORE y2023:1 1
ZSCORE s1_2023:1 1

The C++ operation code snippet is as follows:

	// 获取玩家积分(插件中只获取总榜记录)
	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);
	reply = (redisReply *)redisCommand(pRedis, szCommand);
	if (NULL == reply) goto STEP_END;
	if (REDIS_REPLY_ERROR == reply->type)goto STEP_END;
	if (REDIS_REPLY_STRING == reply->type)
	{
		fScore = strtod(reply->str, NULL);
	}
	if (NULL != reply)
	{
		freeReplyObject(reply);
		reply = NULL;
	}

7. Redis Zrevrank query ranking

    Redis ordered set (sorted set), the Redis Zrevrank command returns the ranking of the members in the ordered set. Among them, the members of the ordered set are sorted by decreasing score value (from large to small); the ranking is based on 0, that is, the member with the largest score value is ranked 0; use the ZRANK command to get the members sorted by increasing score value (from small to large) ranking.

The command prototype is as follows:

Zrevrank key member

key        同上面的武器榜单
member     玩家id


// 查询不同榜单的排名命令:
ZREVRANK a:1 1
ZREVRANK y2023:1 1
ZREVRANK s1_2023:1 1

The C++ operation code snippet is as follows:

	// 获取在玩家排名(插件中只获取总榜记录)
	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);
	reply = (redisReply *)redisCommand(pRedis, szCommand);
	if (NULL == reply) goto STEP_END;
	if (REDIS_REPLY_ERROR == reply->type)goto STEP_END;
	if (REDIS_REPLY_NIL == reply->type)
	{
		nRank = pServer->m_nMaxPlayers;
		bRet = TRUE;
	}
	if (REDIS_REPLY_INTEGER == reply->type)
	{
		nRank = (reply->integer & 0xFFFFFFFF) + 1;
		bRet = TRUE;
	}

	if (NULL != reply)
	{
		freeReplyObject(reply);
		reply = NULL;
	}

3. Summarize and avoid pitfalls

1. Try not to use fixed strings as KEY

When writing rankings, character types like "kill" are not used in the KEY of general weapon data. Usually a number is used instead of aspect expansion, and only the enum macro needs to be modified.’s KEY_NAME, similar to the following This: %u:%u to splice Weapon + data type 

// push数据类别				// 命令     字符	积分
enum EnumPushType{					
	EPT_UNKNOW = '@',		// 未知		64 @	+0
	EPT_KILL,				// 杀敌		65 A	+2
	EPT_SHOT,				// 射击		66 B	+0
	EPT_HEADSHOT,			// 爆头		67 C	+1
	EPT_HIT,				// 击中		68 D	+0

	EPT_DAMAGE,				// 伤害		69 E	+0
	EPT_DEATH,				// 死亡		70 F	-2
	EPT_FIRSTKILL,			// 首杀		71 G	+1
	EPT_FIRSTDEATH,			// 首死		72 H	+0
	EPT_BOMB_DEFUSION,		// 拆除C4	73 I	+2

	EPT_BOMB_PLANTING,		// 安装C4	74 J	+2
	EPT_TIME_ONLINE,		// 在线		75 K	+0  每秒+0.002分(每小时7.2分)

	EPT_KILL_WORLD,			// 摔死		76 L	+0
	EPT_KILL_SELF,			// 自杀次数	77 M	+0

	EPT_MAX_PLAYER,			// 最大玩家 78 N	+0
	EPT_RANK,				// 当前排名 79 O	+0
	EPT_SCORE,				// 当前积分 80 P	+0

	// 身体伤害
	EPT_DMAGE_NONE,			// 击中空枪 81 Q	+0
	EPT_DMAGE_HEAD,			// 击中头部 82 R	+0
	EPT_DMAGE_CHEST,		// 击中胸部 83 S	+0
	EPT_DMAGE_STOMACH,		// 击中胃部 84 T	+0
	EPT_DMAGE_LEFTARM,		// 击中左臂 85 U	+0
	EPT_DMAGE_RIGHTARM,		// 击中右臂 86 V	+0
	EPT_DMAGE_LEFTEG,		// 击中左脚 87 W	+0
	EPT_DMAGE_RIGHTEG,		// 击中右脚 88 X	+0
	EPT_DMAGE_SHIELD,		// 击中盾牌 89 Y	+0

	// 武器+BKILL
	EPT_BKILL,				// 被击杀	90 Z	+0
	EPT_BHEAD,				// 被爆头	91 [	+0
	
	// 击中次数
	EPT_HIT_NONE,			// 击中空枪 92 \	+0
	EPT_HIT_HEAD,			// 击中头部 93 ]	+0
	EPT_HIT_CHEST,			// 击中胸部 94 ^	+0
	EPT_HIT_STOMACH,		// 击中胃部 95 _	+0
	EPT_HIT_LEFTARM,		// 击中左臂 96 `	+0
	EPT_HIT_RIGHTARM,		// 击中右臂 97 a	+0
	EPT_HIT_LEFTEG,			// 击中左脚 98 b	+0
	EPT_HIT_RIGHTEG,		// 击中右脚 99 c	+0
	EPT_HIT_SHIELD,			// 击中盾牌 100 d	+0

	// 混战参数 add by MT 2023-09-30
	EPT_ROUND,				// 总回合
	EPT_RWIN_T,				// 回合:T杀完胜利
	EPT_RWIN_BOOM,			// 回合:T爆炸胜利
	EPT_RWIN_CT,			// 回合:CT杀完胜利
	EPT_RWIN_DEFUSE,		// 回合:CT爆炸胜利
	EPT_RWIN_SAVED,			// 回合:CT时间结束胜利
	EPT_RWIN_RESCUE,		// 回合:CT解救人质胜利
	EPT_RWIN_NOT_RESCUE,	// 回合:CT未解救人质胜利
	EPT_RWIN_TYPE1,			// 回合:保留胜利1
	EPT_RWIN_TYPE2,			// 回合:保留胜利2

	EPT_RLOSE,				// 失败回合
	EPT_REVEN,				// 平局回合

	// 比赛参数
	EPT_SESSION,			// 总场次
	EPT_SWIN,				// 胜利场次
	EPT_SLOSE,				// 失败场次
	EPT_SEVEN,				// 平局场次

	EPT_MVP,				// 最佳次数
	EPT_RWS,				// 每局贡献评分(伤害占比)

	// 每回合杀敌统计
	EPT_KILL_0,				// 0K 酱油局
	EPT_KILL_1,				// 1K
	EPT_KILL_2,				// 2K
	EPT_KILL_3,				// 3K
	EPT_KILL_4,				// 3K
	EPT_KILL_5,				// 5K
	EPT_KILL_6,				// 6K
	EPT_KILL_7,				// 7K
	EPT_KILL_8,				// 8K
	EPT_KILL_9,				// 9K
	EPT_KILL_10,			// 10K
	EPT_KILL_11,			// 11K
	EPT_KILL_12,			// 12K
	EPT_KILL_13,			// 13K
	EPT_KILL_14,			// 14K
	EPT_KILL_15,			// 15K
	EPT_KILL_16,			// 16K

	// 残局统计
	EPT_1V1,				// 1v1
	EPT_1V2,				// 1v2
	EPT_1V3,				// 1v3
	EPT_1V4,				// 1v4
	EPT_1V5,				// 1v5
	EPT_1V6,				// 1v6
	EPT_1V7,				// 1v7
	EPT_1V8,				// 1v8
	EPT_1V9,				// 1v9
	EPT_1V10,				// 1v10
	EPT_1V11,				// 1v11
	EPT_1V12,				// 1v12
	EPT_1V13,				// 1v13
	EPT_1V14,				// 1v14
	EPT_1V15,				// 1v15
	EPT_1V16,				// 1v16

	// 残局信息
	EPT_1ROUND,				// 残局场次
	EPT_1RWIN,				// 残局胜利

	EPT_ASSIST,				// 助攻次数
	EPT_ADR,				// 场均实际伤害占比累计

	// 穿墙信息
	EPT_WALL_HIT,			// 穿墙累计命中次数
	EPT_WALL_DAMAGE,		// 穿墙累计射击伤害
	EPT_WALL_HEAD,			// 穿墙累计爆头次数
	EPT_WALL_KILL,			// 穿墙累计击杀次数

	EPT_BWALL_HIT,			// 被穿墙累计命中次数
	EPT_BWALL_DAMAGE,		// 被穿墙累计射击伤害
	EPT_BWALL_HEAD,			// 被穿墙累计爆头次数
	EPT_BWALL_KILL,			// 被穿墙累计击杀次数
};

 This macro corresponds to a double array to calculate the score, so only one such function is needed as the gateway's main interface:

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


2. Try to use transactional reading and writing

Finally, please note that using transaction processing can speed up reading and writing efficiency by more than 10 times. The sample code is as follows:

	// 新版采用事务方式读取,加快速度
	for (i = 0; i < MAX_WEAPONS; i++)
	{
		nWeapon = i + EPT_UNKNOW;
		for (j = 0; j < MAX_OPT_COUNT; j++)
		{
			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);
			redisAppendCommand(pRedis, szCommand); // 事务读取
		}
	}

    // 遍历所有武器数据
	for (i = 0; i < MAX_WEAPONS; i++)
	{
		// 遍历所有参数
		for (j = 0; j < MAX_OPT_COUNT; j++)
		{
			nRet = redisGetReply(pRedis, (void **)&reply);
			if (nRet != REDIS_OK)
				continue;

			if (reply->type == REDIS_REPLY_STRING)
			{
				sData.m_nValue[i][j] = atoi(reply->str);
			}
			if (reply->type == REDIS_REPLY_INTEGER)
			{
				sData.m_nValue[i][j] = reply->integer & 0xFFFFFFFF;
			}
		}
	}

Guess you like

Origin blog.csdn.net/wangningyu/article/details/133756015