游戏合服时如何避免主键冲突

41次阅读

共计 2505 个字符,预计需要花费 7 分钟才能阅读完成。

Last-Modified: 2019 年 5 月 10 日 15:23:31

背景

滚服类型的游戏常见于 手游、网游(包括 H5), 滚服类型游戏的特点(与传统大服架构区别):

  • 单服同时在线游戏人数少(eg. 3000 人), 达到上限就开新服

以下这部分内容来自: https://www.cnblogs.com/youji…

滚服模式是游戏类型,技术架构和急功近利的坑钱策略等因素共同决定的,大服游戏包括绝大部分端游,以及类 COC 这样类型的游戏。

另外,虽然像英雄联盟,王者荣耀这样的游戏也分服架构,但是这个并不是我理解中的“滚服游戏“,首先他们虽然分服,但是每个服的人数上限也是可以高达几十万,他们并不会发生频繁的合服情况。

而滚服游戏更多是通过游戏策略设计,鼓励玩家花钱走捷径透支游戏生命周期,甚至几天即可独霸一个服务器。从而导致其他玩家望尘莫及,即使是花钱追也性价比极低,还不如进入一个新服重新开始。

这就导致了新服一开,玩家即蜂拥而至,争先恐后练级升装备,以求最快速进入排行榜前列,如果努力一番发现落后了,可能就只能坐等下一个新服。这也导致了新服人数火爆,老服慢慢变成人烟凋零的村服,甚至没人的死服。

为了能够节约服务器带宽资源,同时让少数剩余的玩家能够玩得起来,就必须要要进行频繁的合服,把若干个互不相干的服务器玩家,合并到一个服里面;这样又开启一波玩家竞争和收割。

合服处理

合服时要特别注意:

  1. 防止主键冲突
  2. 防止唯一 (unique) 键不冲突 (eg. 用户昵称)
  3. 清空僵尸数据 / 无效玩家数据(小心数据残留, 避免数据不一致)
  4. insert into 时注意字段顺序不一致问题

处理主键冲突的办法主要有 2 种:

  1. 合服前预处理冲突键
  2. 开服时预分配好可能的冲突键, 合服时则无需额外处理(推荐)

防止主键冲突

合服时处理冲突

如果在一开始没有设计好数据库的话, 合服时很容易遇到的普遍情况就是: 主键冲突

游戏通常有角色表, 道具表, 一般都是用数据库的自增长 (AUTO_INCREMENT) 特定来创建其主键, 以此保证主键的唯一,以如下表结构为例,id 只能保证在本服中唯一,A 服中有个玩家 id 是 1, B 服中也有个玩家 id 是 1, 合服前必须解决这个冲突.

-- 玩家表
CREATE TABLE `users`(`id` int(11) unsigned not null,
    `name` varchar(50) default null,
    primary key (`id`)
) Engine=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

-- 玩家道具表
CREATE TABLE `props`(`id` int(11) unsigned not null,
    `user_id` int(11) unsigned not null,
    primary key (`id`)
) Engine=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

通常做法是给 B 表中的所有 users.id 字段值加上一个基数(max('A.users.id'), 同时还要修改涉及到的其他表, 比如 props.user_id 字段必须相应修改, 否则无法关联到对应玩家,因为修改了 users.id 后一般要修改相应的数十张表中的 user_id, 同时若使用了外键还得额外处理外键的删除和重做.

实际上这种处理很繁琐,因为通常需要修改主键的不只一张表,表的关系越复杂,修改一个表的主键牵涉到的相关表越多,实际操作时层层嵌套会很恶心 …

开服时预分配防止后续冲突

一种更为推荐的办法是在一开始设计好主键,提前规避冲突,这样在合服操作时就可以无脑数据合并。

  1. 使用自增值, 服编号在前

每个服都有一个唯一的服编号,可以利用这一点,提前规划好每个服的 主键区域。

比如 users 表主键 id, 当存在 1 万个玩家时, id取值范围为 1~10000。

先定个小目标, 我们预估单个服玩家数量不会超过 1 一个亿, 因此用服编号乘以这个量级, 因此 1 服的主键 id 范围 100000000 ~ 199999999, 111 服的是 11100000000 ~ 11199999999,

注意这已经超出 int 表示范围, 因此必须使用 bigint, 若使用 unsigned bigint可以完整表示 19 位数。如果你觉得可能会超出小目标,那可以把这个量级再调大一点,比如 100 个亿。

关于 mysql 数字的数据类型可以看一下这边: https://www.cnblogs.com/yiwd/…

牺牲一点硬盘空间来规避后续合服的恶心事项,我是觉得很划算。

  1. 不完全使用自增值, 服编号在后

还有另外一种预分配方案,即一开始预估服编号的范围(比如 1~9999),将服编号作为 users.id 后 N 位,这样可以避免玩家看到自己一大长串的 uid 觉得恶心。比如 1 服玩家原 id 为 23 的玩家, 按照这种方案, 其 id 就是 230001, 111 服的 id 为 23 的玩家则是 230111.

这种方案就是在生成主键 id 时会绕一点, 但对于 MongoDB 这一类的倒无所谓了.

  1. 使用 uuid 作为主键

优点:确保全局唯一,不仅是表唯一,而且是库唯一,很方便不同数据库间迁移

这种方案不好的地方在于 UUID 的无序性, 会导致 InnoDB 引擎产生巨大的 IO 压力,这根 InnoDB 主键索引与数据存储位置相关。

字段顺序不一致

游戏版本迭代更新容易导致不同数据库结构有所差异, 因此在合服时, 一个是确保数据库版本一致.

如果是简单地使用 insert into db2.table2 select * from db1.table1 时要确保字段是一致的.

如果不一致就乖乖指定字段 INSERT INTO new_db.table_name(column1, column2,...) (SELECT column1, column2,... FROM old_db.table_name);

具体有哪些字段可以通过如下语句查出:

select COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_NAME='table' and TABLE_SCHEMA='database';

正文完
 0