游戏服务器之文件数据库用于数据服务器的存取档案。
设计思路:
1、业务线程
数据服务器进程的业务线程有3个:
1)本地数据文件写线程
2)mysql存取线程
3)备份文件压缩线程
其中,本地数据文件写线程的业务是负责文件数据库的写入。
2、文件结构
数据库文件类型分为:
1)索引文件
2)数据文件
(1)索引文件
索引文件的结构包括:文件头描述(FDBHeader ) 和 所有的数据块描述结构(ChunkDesc )。
FDBHeader :描述ChunkDesc 的数量和单位大小
ChunkDesc :描述一个数据区头加数据区数据(DataHeader + RecordData)的文件偏移、有效数据大小和数据块大小
索引文件内容结构:FDBHeader + ChunkDesc +ChunkDesc + ChunkDesc +...
(1-1)文件头描述
索引文件开始为数据库文件头。
数据库文件头结构:
struct FDBHeader
{
FDBHeader()
{
memset(this, 0, sizeof(FDBHeader));
}
static const DWORD IDENT = MAKEFOURCC('F', 'D', 'I', 0);
static const DWORD VERSION = 0x010B0A0D;
//文件标识固定为:MAKEFOURCC('F', 'D', 'B', 0)
DWORD dwIdent;
//数据库文件格式版本号,目前为0x010B0A0D
DWORD dwVersion;
//数据库中存储的记录数量
INT nRecordCount;
//数据记录块单位大小(1024的倍数)
DWORD dwChunkSize;
//保留字节
char btReseves[48];
};
(1-2)文件记录索引
数据库文件头之后是所有的文件描述块(ChunkDesc )。
每个文件记录索引含一个块描述结构struct ChunkDesc 和 本索引记录在索引文件中的偏移位置(读文件时记录)。
文件记录索引的数量是文件头描述中的记录数nRecordCount, 在索引文件中占的大小为sizeof(ChunkDesc) * hdr.nRecordCount。
数据块索引结构:
struct ChunkIndex
{
#pragma pack(push, 1)
struct ChunkDesc
{
INT64 nId;//数据记录唯一ID值。如果值为零则表示该记录为一个空闲的数据块,可以被回收利用。
INT64 nDataOffset;//记录在数据库文件中的绝对偏移值
INT64 nDataSize;//数据记录字节数(包含数据块开头的DataHeader的大小)
INT64 nChunkSize;//数据记录块大小。如果值为0则表示该记录为没有任何意义,该索引在文件中的位置空间可以被回收利用。
}chunk;
#pragma pack(pop)
INT64 nIndexOffset;//本索引记录在索引文件中的偏移位置
};
(2)数据文件
数据文件内容为所有文件记录数据。
每个文件记录数据有记录数据头和记录数据体(每个数据块均以一个数据记录头开始,接着数据记录体):
数据文件:DataHeader+ DataBody (section header + section data)+DataHeader+ DataBody +...
数据文件类型包括角色描述数据文件(CharDesc)、角色数据数据文件(CharData)、帮会数据数据文件(GuildData)。
加载到内存的是角色描述数据,其他数据需要读取数据文件。
需要自动创建数据库目录(如char 角色数据文件,逐层创建数据库目录,例如,角色数据库文件./FDB/db1/char,则需要创建目录./FDB以及./FDB/db1)。
(2-1)记录数据头
数据记录头:
struct DataHeader
{
INT64 dwIdent;//数据记录头标志,固定为0xFFAADD8800DDAAFF
INT64 nId;//数据ID
INT64 nDataSize;//数据长度(不含本记录头)
};
(2-2)记录数据体
数据包数据内容,具体的数据记录内容。
数据记录内容:记录数据区头 + 记录数据区数据 + 记录数据区头 + 记录数据区数据 + ...
每个区是一个数据类型(道具、任务、技能...)
/**********************************************************************************
* 角色、帮会数据存储格式
* +-------------------+-----------------+-------------------+-----------------+
* | section(1) header | section(1) data | section(2) header | section(2) data |
* +-------------------+-----------------+-------------------+-----------------+
* 角色、帮会数据存储中的数据项头结构
*********************************************************************************/
struct DataSectionHeader
{
unsigned short wDataType;//数据类型
unsigned short wDataVersion;//数据版本号
unsigned short wStructSize;//数据结构大小
unsigned short wDataCount;//数据结构数量
};
3、内存结构
(1)文件描述符
每个自定义文件对象在开启文件时,会创建一个索引文件描述符(对应索引文件)、一个数据文件描述符(对应数据文件)。
(2)索引列表
包括三种类型数据索引列表:
1)有效数据索引列表
2)空闲数据索引列表(空闲数据大小索引列表和空闲数据偏移索引列表)
3)无效数据索引列表
每个自定义文件对象在开启文件时,会读取索引文件的内容到有效数据索引列表和空闲数据索引列表(对于空闲数据块)和无效数据索引列表(对于无效的索引)。
所有的索引对象是用对象分配器分配的。
(3)数据写入
根据数据记录描述的唯一ID值来查询有效数据索引列表,若找到就更新数据文件内容(写入数据文件中的对应的数据头和数据体);若找不到就新建索引对象,
并写到数据文件末尾(数据头和数据体)和索引文件。
(4)数据查询
根据数据记录描述的唯一ID值来查询有效数据索引列表:
1)找到有效数据索引就根据有效数据索引(含数据记录在数据文件中的绝对偏移值和数据记录字节数大小)读取数据文件中的对应位置的数据内容;
2)找不到则查找失败。
(5)数据缓存列表
角色描述信息的数据文件的内容需要缓存在内存的三个有序列表:
1)基于角色ID排序的角色描述列表
2)基于角色名称排序的角色描述列表
3)基于账号ID排序的角色描述列表
开启角色描述文件数据库时加载,以后新增角色需要往三个有序列表添加。
具体实现:
1、数据记录块
数据记录块是用来描述数据存档记录的位置和大小的。在内存中,数据记录块的描述是以数据块索引的形式存在于有效数据索引列表中。
(1)数据记录块大小
如果当前块过大,就需要将该数据块分成两块。
如果当前块空间不足容纳新数据,则必须申请一个新的块来存放数据并且将当前块添加为空闲块.
数据记录块单位大小可用于优化数据块分配。
数据记录块单位大小:
记录块大小用于预保留数据记录的空间,以便优化在记录内数据长度变大时的存储效率。数据库存储记录数据时,会保证用于对一个记录的字节长
度是dwChunkSize(数据块单位大小)的倍数。例如在创建数据库时指定记录块大小为1024,那么向数据库存储一个长度为800字节的记录时,仍然会给此记录分配长度
为1024字节的空间,存储1025字节的记录时,则会分配2048字节的空间。这将有利于在记录的数据长度会不断变化的场合,预先为下次变化保留存储
空间,而合理的提供数据记录块大小值,则能充分的体现这一优化效果。dwChunkSize在数据库创建的时候既被指定,并且此后不得再改变。
ps:
角色描述数据的数据块单位大小是64字节,角色数据的数据块单位大小是2048字节,公会数据的数据块单位大小是1024字节。
(2)数据记录块索引
一个数据块索引标识一个数据块。
内存中的数据块索引(在索引文件中的记录块描述):
struct ChunkIndex
{
#pragma pack(push, 1)
struct ChunkDesc
{
INT64 nId;//数据记录唯一ID值。如果值为零则表示该记录为一个空闲的数据块,可以被回收利用。
INT64 nDataOffset;//记录在数据库文件中的绝对偏移值
INT64 nDataSize;//数据记录字节数(包含数据块开头的DataHeader的大小)
INT64 nChunkSize;//数据记录块大小。如果值为0则表示该记录为没有任何意义,该索引在文件中的位置空间可以被回收利用。
}chunk;
#pragma pack(pop)
INT64 nIndexOffset;//本索引记录在索引文件中的偏移位置
};
2、数据块的分配管理
(1)数据记录块拆分策略
将大的数据块拆分为小数据块的条件为:
1)剩余空间大小需要大于等于128字节
2)数据块写入新数据后剩余空间大于数据库头中的数据块单位大小;
3)新数据长度是数据块长度的一半以内;
确定好要分出新的数据块时,就将新数据块的大小调整为文件头中规定的数据库单位大小的倍数,且每次分出的数据不会超出原来的数据的大小
nChunkSize:数据块大小
dwDataSize:新写入数据大小
INT64 CCustomFileDB::getChunkSplitOutSize(INT64 nChunkSize, INT64 dwDataSize) const
{
INT64 nRemainSize = nChunkSize - dwDataSize;//剩余空间大小
if (nRemainSize >= 128 && (nRemainSize > m_Header.dwChunkSize) && (dwDataSize <= nChunkSize / 2) )
{
//将新数据块的大小调整为头中规定的数据库单位大小的倍数
return nRemainSize / m_Header.dwChunkSize * m_Header.dwChunkSize;
}
//返回0表示无法分割
return 0;
}
(2)分配数据记录块
在文件中分配一个新的数据块。
获取数据块:
1)需要的数据块的大小是数据块单位大小的整数倍。
2)在空闲数据的列表里面找一个合适大小的数据块的索引,一个大小最接近的数据块索引。
3)空闲数据的列表里没有合适的块,就使用新的数据块(新的索引,新的数据块会从数据文件末尾开始)
CCustomFileDB::ChunkIndex* CCustomFileDB::allocChunk(INT64 nId, INT64 dwDataSize)
{
ChunkIndex *pResult = NULL;
INT64 nChunkSize = 0;
//当dwDataSize为0时表示仅申请一个索引对象,不预留空间。
if (dwDataSize != 0)
{
//根据数据块单元大小调整nChunkSize(为数据块单位大小的整数倍)
if (m_Header.dwChunkSize > 0)
nChunkSize = dwDataSize + (m_Header.dwChunkSize - (dwDataSize % m_Header.dwChunkSize));
else nChunkSize = dwDataSize;
//在空闲列表中找一个大小最接近的数据块索引
//列表的查找算法是:有排序的就使用折半查找,否则就从后往前一个个查找
INT_PTR nIdx = m_FreeDataSizeList.searchMiniGreater(nChunkSize);//空闲数据大小排序表,用于快速找到一个合适的空数据位置
if (nIdx > -1)//找到需要的空闲数据块的索引
{
FreeDataOffsetIndex offsetIndex;
pResult = m_FreeDataSizeList[nIdx].pIndex;
Assert(pResult->chunk.nChunkSize >= nChunkSize);
offsetIndex.pIndex = pResult;
//空