网游内存数据库的设计(1)
网络游戏的数据变动比较频繁,如果每次数据变动都刷往后端数据库,会导致数据库不负重荷。在游戏逻辑和数据库间提供一层缓冲服务,有利于减轻这重压力.
首先,网络游戏的数据在数据库中是以表的形式保存的,每个玩家的数据占用其中的一行或几行.以玩家基本属性为例:
基本表: chainfo
表结构:chaid,chaname,hp,mp,maxhp,maxmp ...
为此,内存数据库将建立针对行集和行数据的抽象。为了提高查询的效率,在内存中建立一个大的hash-table,hash-table中只支持两种数据结构:变长的list和定长
的array.list用以表示集,array表示数据行.根据建立的逻辑索引,数据库中的一个表,在hash-table中可能会存放在多处.以玩家任务表为例:
chaid,missionid ...
chaid和missionid一起建立了一个唯一的数据库索引,但可以为它建立两个逻辑索引,chaid和chaid,missionid.
这样,当用户上线时(假设用户id为1001),将导入所有chaid==1001的行,在hash-table中建立一个list,这个list中的每个元素都是一个array,每个array表示一个任
务记录行,list就是这个玩家所有任务的集合,如果游戏逻辑需要获取这个玩家的任务列表,可以通过以下key获取"mission,chaid,1001".当然仅有一个行集是不够的,
因为当用户的某个任务数据变动时,必须遍历list,找到正确的array再将变动更新到正确的array中.而网络游戏中最频繁的就是更新操作了,为了提高效率,为每一个
任务行建立一个逻辑索引"chaid,missionid",将任务对应的数据行也存放在hash-table中,这样如果1001号玩家希望改变他的2号任务的数据,则可以发key="mission,
chaid,missionid,1001,2"后跟变更数据.来改变2号任务的数据.
下面贴出代码片段,介绍核心的存储数据结构.
enum{
INT8 = 0,
INT16,
INT32,
INT64,
DOUBLE,
STRING,
BINARY,
};
typedef struct basetype
{
int8_t type;//the real type
void *data;
}*basetype_t;struct db_type_string
{ struct basetype base;
int32_t size;
};struct db_type_binary
{ struct basetype base;
int32_t size;
};
首先是基本的数据元素,也就是array可以存放的元素类型,分别是4种整型,double,字符串和二进制数据.
enum{
DB_LIST = 1,
DB_ARRAY,
};
typedef struct db_element
{ struct refbase ref;
int32_t hash_index;//index in global_table int8_t type;
}*db_element_t;
db_element_t db_element_acquire(db_element_t,db_element_t);void db_element_release(db_element_t*);//represent a db rowtypedef struct db_array
{ struct db_element base;
int32_t size;
basetype_t* data;
}*db_array_t;
db_array_t db_array_create(int32_t size);
db_array_t db_array_acquire(db_array_t,db_array_t);void db_array_clear(db_array_t);//clear the datavoid db_array_release(db_array_t*);//get/set one element of the db rowbasetype_t db_array_get(db_array_t,int32_t index);void db_array_set(db_array_t,int32_t index,basetype_t);struct db_node
{
list_node next;
db_array_t array;
};//represent db row settypedef struct db_list
{ struct db_element base; struct link_list *l;
}*db_list_t;
db_list_t db_list_create();
db_list_t db_list_acquire(db_list_t,db_list_t);void db_list_release(db_list_t*);
int32_t db_list_append(db_list_t,db_array_t);
int32_t db_list_size(db_list_t);
int32_t db_list_shrink(db_list_t);
然后是array和list的定义,他们都继承自db_element_t,而hash_table中的元素正是db_element_t.array和list都实现了引用计数,这样当所有引用都释放时,可以被正
确的销毁。这里要注意的是,一个array可能被存放在多个list中,这样,当一个数据行被删除时,必须让所有的list知道这个数据已经无效。我的做法不是在array被删
除时通知所有的list删除对应的array,而是通过db_array_clear,清除array中存放的有效数据。然后通过一个算法,定期对数据占用最多的list执行shrink,以销毁失效的
array.
typedef struct global_table *global_table_t;
global_table_t global_table_create(int32_t initsize);void global_table_destroy(global_table_t*);
db_element_t global_table_find(global_table_t,const char *key);
int32_t global_table_remove(global_table_t,const char *key);
int32_t global_table_add(global_table_t,const char *key,db_element_t e);//collect unused db_element_tvoid global_table_shrink(global_table_t);
然后是hash_table的定义,只向外提供三个操作接口,分别是查找,删除和添加.对于添加操作,如果最开始往一个hash slot添加的是一个array,当再次往这个slot添加
一个array时,将会自动将slot中的元素从array提升为list,并将两个array都添加到这个list中.
下面是一些测试代码:
#include <stdio.h>#include "global_table.h"int main()
{
global_table_t gtb = global_table_create(1024);
db_array_t a1 = db_array_create(3);
db_array_t a2 = db_array_create(3);
db_array_t a3 = db_array_create(3);
db_array_t a4 = db_array_create(3);
db_array_set(a1,0,basetype_create_int32(10));
db_array_set(a1,1,basetype_create_int32(11));
db_array_set(a1,2,basetype_create_int32(12));
db_array_set(a2,0,basetype_create_int32(20));
db_array_set(a2,1,basetype_create_int32(21));
db_array_set(a2,2,basetype_create_int32(22));
db_array_set(a3,0,basetype_create_int32(30));
db_array_set(a3,1,basetype_create_int32(31));
db_array_set(a3,2,basetype_create_int32(32));
db_array_set(a4,0,basetype_create_int32(40));
db_array_set(a4,1,basetype_create_int32(41));
db_array_set(a4,2,basetype_create_int32(42));
global_table_add(gtb,"kenny",(db_element_t)a1);
global_table_add(gtb,"kenny",(db_element_t)a2);
global_table_add(gtb,"kenny",(db_element_t)a3);
global_table_add(gtb,"kenny",(db_element_t)a4);
global_table_add(gtb,"kenny1",(db_element_t)a1);
global_table_add(gtb,"kenny2",(db_element_t)a2);
global_table_add(gtb,"kenny3",(db_element_t)a3);
global_table_add(gtb,"kenny4",(db_element_t)a4);
//test search
db_list_t l = (db_list_t)global_table_find(gtb,"kenny");
printf("the row size of kenny(a db_list_t):%dn",db_list_size(l));
printf("element of a1:key(kenny1):");
db_array_t _a = (db_array_t)global_table_find(gtb,"kenny1"); int i = 0; for( ; i < 3; ++i)
{
basetype_t b = db_array_get(_a,i);
printf("%d ",basetype_get_int32(b));
}
printf("n");
printf("element of a2:key(kenny2):");
_a = (db_array_t)global_table_find(gtb,"kenny2");
i = 0; for( ; i < 3; ++i)
{
basetype_t b = db_array_get(_a,i);
printf("%d ",basetype_get_int32(b));
}
printf("n");
printf("element of a3:key(kenny3):");
_a = (db_array_t)global_table_find(gtb,"kenny3");
i = 0; for( ; i < 3; ++i)
{
basetype_t b = db_array_get(_a,i);
printf("%d ",basetype_get_int32(b));
}
printf("n");
printf("element of a4:key(kenny4):");
_a = (db_array_t)global_table_find(gtb,"kenny4");
i = 0; for( ; i < 3; ++i)
{
basetype_t b = db_array_get(_a,i);
printf("%d ",basetype_get_int32(b));
}
printf("n");
db_array_release(&a4);
global_table_remove(gtb,"kenny4"); /* shrink will cause the refcount of a4 reduce to zero,
* then a4 will be destroyed */
db_list_shrink(l);
printf("the row size of kenny(a db_list_t),after remove and shrink: %dn",db_list_size(l));
db_array_release(&a1);
db_array_release(&a2);
db_array_release(&a3);
printf("destroy global table,this will cause all element destroyedn");
global_table_destroy(>b);
return 0;
}
本篇仅仅介绍了核心的数据结构,后端的数据库交互策略,网络前端,备份处理和分布式多缓存将在后面慢慢介绍.
代码地址:https://github.com/sniperHW/kendylib dbcahce目录
- (66) 理解synchronized / 计算机程序的思维逻辑
- 用Python搭建一个校园维基网站(一)
- (67) 线程的基本协作机制 (上) / 计算机程序的思维逻辑
- 制作Aspose CHM文档的过程记录
- 用python搭建一个校园维基网站(二)—— 可编辑内容的首页的创建
- Django博客教程(四):让 django 完成翻译—迁移数据库模型
- Calendar类中add/set/roll方法的区别
- 如何构建一个分布式爬虫(理论篇)
- Python微型Web框架Bottle源码分析
- VirtualBox相关问题总结
- Java 枚举7常见种用法
- ALI的Tensorflow炼成与GAN科普
- LaTeX内容总结
- (60) 随机读写文件及其应用 - 实现一个简单的KV数据库 / 计算机程序的思维逻辑
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Centos在线迁移到腾讯云cvm
- ClickHouse源码导读:网络IO
- 手把手教你:将ClickHouse集群迁至云上
- 直播中台iLiveSDK终端框架演变之路
- ClickHouse 数据导入实战:Kafka 篇
- pipeAsyncFunctions
- 【python-leetcode287-循环排序】寻找重复的数
- 聊聊claudb的list command
- 【python-leetcode448-循环排序】找到所有数组中消失的数字
- input/change/composition/keydown事件详解
- isSameDate
- 【python-leetcode57-区间合并】插入区间
- 【python-leetcode56-区间合并】合并区间
- 【python-leetcode876-快慢指针】链表的中间节点
- vuejs之springboot+vue+element-ui之分页显示相关信息