游戏服务器程序中,经常需要生成全局的唯一ID号,这个功能很常用,本文将介绍一种通用ID生成组件。游戏服务器程序中使用此组件的场景有:
- 创建角色时,为其分配唯一ID
- 创建物品时,每个物品需要唯一ID
- 创建宝宝、灵兽时需要唯一ID
原理介绍
ID生成器的原理就是使用全局整型变量,每次分配之后该变量递增1。由于服务器重启后全局变量失效,故全局变量需要持久化保存,相应的,服务器启动时从持久化中载入全局变量。ID生成器的工作流程为:
- 建议采用数据库作为持久化存储,本文以mysql为例
- 启动时从数据库载入全局变量,作为分配的起始值
- 每次分配id前,先递增全局变量
- 每次递增后,更新数据库中的全局变量值
- 由于相同的功能模块可能在不同的GameServer上运行,故唯一ID号使用64位整型,其中16位用来表示GameServerID
- 由于不同的功能即使ID号相同互不影响,如角色ID和物品ID实际上是独立的互不冲突的,所以全局变量可以分类型,不同的类型使用不同全局变量。
示例代码
sql = """ create table id_generator ( AUTO_INC_ID bigint not null, TYPE int not null, SERVER_ID int not null, RUNING_FLAG int not null, primary key(TYPE, SERVER_ID) ) """ class idgen_t: def __init__(self, db_host_, type_id_ = 0, server_id_ = 0): self.type_id = type_id_ self.server_id = server_id_ self.auto_inc_id = 0 self.db_host = db_host_ self.db = None self.saving_flag = False self.runing_flag = 0 def init(self): self.db = ffext.ffdb_create(self.db_host) ret = self.db.sync_query("SELECT `AUTO_INC_ID`, `RUNING_FLAG` FROM `id_generator` WHERE `TYPE` = '%d' AND `SERVER_ID` = '%d'" % (self.type_id, self.server_id)) #print(ret.flag, ret.result, ret.column) if len(ret.result) == 0: #数据库中还没有这一行,插入 self.db.sync_query("INSERT INTO `id_generator` SET `AUTO_INC_ID` = '0',`TYPE` = '%d', `SERVER_ID` = '%d', `RUNING_FLAG` = '1' " % (self.type_id, self.server_id)) return True else: self.auto_inc_id = int(ret.result[0][0]) self.runing_flag = int(ret.result[0][1]) if self.runing_flag != 0: self.auto_inc_id += 10000 ffext.ERROR('last idgen shut down not ok, inc 10000') self.db.sync_query("UPDATE `id_generator` SET `RUNING_FLAG` = '1' WHERE `TYPE` = '%d' AND `SERVER_ID` = '%d'" % (self.type_id, self.server_id)) #if self.auto_inc_id < 65535: # self.auto_inc_id = 65535 return True def cleanup(self): db = ffext.ffdb_create(self.db_host) now_val = self.auto_inc_id db.sync_query("UPDATE `id_generator` SET `AUTO_INC_ID` = '%d', `RUNING_FLAG` = '0' WHERE `TYPE` = '%d' AND `SERVER_ID` = '%d'" % (now_val, self.type_id, self.server_id)) return True def gen_id(self): self.auto_inc_id += 1 self.update_id() low16 = self.auto_inc_id & 0xFFFF high = (self.auto_inc_id >> 16) << 32 return high | (self.server_id << 16)| low16 def dump_id(self, id_): low16 = id_ & 0xFFFF high = id_ >> 32 return high << 16 | low16 def update_id(self): if True == self.saving_flag: return self.saving_flag = True now_val = self.auto_inc_id def cb(ret): #print(ret.flag, ret.result, ret.column) self.saving_flag = False if now_val < self.auto_inc_id: self.update_id() self.db.query("UPDATE `id_generator` SET `AUTO_INC_ID` = '%d' WHERE `TYPE` = '%d' AND `SERVER_ID` = '%d' AND `AUTO_INC_ID` < '%d'" % (now_val, self.type_id, self.server_id, now_val), cb) return
总结
- 如果不同区组的GameServer使用不同的ID,那么即使合区,那么老的ID仍然可以工作
- 全局变量递增后,立即更新db,保证尽量保证db和内存的一致性,但是保证同一时间只有一个db 操作执行,db update回调后检查若又被修改后,再次执行db update。比如一次分配了100个id,避免出现“惊群”的db update。
- 由于递增id后立即执行了db update,几乎可以保证db和内存的一致。如果出现运行期宕机,可能会出现db和内存的不一致。可以在此基础上做一个加强版,就是数据库中的每一行都加一个字段running,每次db update都设置为0,服务器正常关闭的时候设置为1。启动服务器载入全局变量时,若该值为0,则在此基础上增加10000。这样可以保证所有的ID都不会重复。
更多精彩文章 http://h2cloud.org