游戏服务器之文件数据库用于数据服务器的存取档案。
设计思路:
数据服务器进程的业务线程有3个:
1)本地数据文件写线程
2)mysql存取线程
3)备份文件压缩线程
其中,本地数据文件写线程的业务是负责文件数据库的写入。
数据库文件类型分为:
1)索引文件
2)数据文件
索引文件的结构包括:文件头描述(FDBHeader ) 和 所有的数据块描述结构(ChunkDesc )。
FDBHeader :描述ChunkDesc 的数量和单位大小
ChunkDesc :描述一个数据区头加数据区数据(DataHeader + RecordData)的文件偏移、有效数据大小和数据块大小
索引文件内容结构:FDBHeader + ChunkDesc +ChunkDesc + ChunkDesc +...
索引文件开始为数据库文件头。
数据库文件头结构:
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]; };
数据库文件头之后是所有的文件描述块(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;//本索引记录在索引文件中的偏移位置 };
数据文件内容为所有文件记录数据。
每个文件记录数据有记录数据头和记录数据体(每个数据块均以一个数据记录头开始,接着数据记录体):
数据文件:DataHeader+ DataBody (section header + section data)+DataHeader+ DataBody +...
数据文件类型包括角色描述数据文件(CharDesc)、角色数据数据文件(CharData)、帮会数据数据文件(GuildData)。
加载到内存的是角色描述数据,其他数据需要读取数据文件。
需要自动创建数据库目录(如char 角色数据文件,逐层创建数据库目录,例如,角色数据库文件./FDB/db1/char,则需要创建目录./FDB以及./FDB/db1)。
数据记录头:
struct DataHeader { INT64 dwIdent;//数据记录头标志,固定为0xFFAADD8800DDAAFF INT64 nId;//数据ID INT64 nDataSize;//数据长度(不含本记录头) };
数据包数据内容,具体的数据记录内容。
数据记录内容:记录数据区头 + 记录数据区数据 + 记录数据区头 + 记录数据区数据 + ...
每个区是一个数据类型(道具、任务、技能...)
/********************************************************************************** * 角色、帮会数据存储格式 * +-------------------+-----------------+-------------------+-----------------+ * | 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;//数据结构数量 };
每个自定义文件对象在开启文件时,会创建一个索引文件描述符(对应索引文件)、一个数据文件描述符(对应数据文件)。
包括三种类型数据索引列表:
1)有效数据索引列表
2)空闲数据索引列表(空闲数据大小索引列表和空闲数据偏移索引列表)
3)无效数据索引列表
每个自定义文件对象在开启文件时,会读取索引文件的内容到有效数据索引列表和空闲数据索引列表(对于空闲数据块)和无效数据索引列表(对于无效的索引)。
所有的索引对象是用对象分配器分配的。
根据数据记录描述的唯一ID值来查询有效数据索引列表,若找到就更新数据文件内容(写入数据文件中的对应的数据头和数据体);若找不到就新建索引对象,
并写到数据文件末尾(数据头和数据体)和索引文件。
根据数据记录描述的唯一ID值来查询有效数据索引列表:
1)找到有效数据索引就根据有效数据索引(含数据记录在数据文件中的绝对偏移值和数据记录字节数大小)读取数据文件中的对应位置的数据内容;
2)找不到则查找失败。
角色描述信息的数据文件的内容需要缓存在内存的三个有序列表:
1)基于角色ID排序的角色描述列表
2)基于角色名称排序的角色描述列表
3)基于账号ID排序的角色描述列表
开启角色描述文件数据库时加载,以后新增角色需要往三个有序列表添加。
具体实现:
数据记录块是用来描述数据存档记录的位置和大小的。在内存中,数据记录块的描述是以数据块索引的形式存在于有效数据索引列表中。
如果当前块过大,就需要将该数据块分成两块。
如果当前块空间不足容纳新数据,则必须申请一个新的块来存放数据并且将当前块添加为空闲块.
数据记录块单位大小可用于优化数据块分配。数据记录块单位大小:
记录块大小用于预保留数据记录的空间,以便优化在记录内数据长度变大时的存储效率。数据库存储记录数据时,会保证用于对一个记录的字节长
度是dwChunkSize(数据块单位大小)的倍数。例如在创建数据库时指定记录块大小为1024,那么向数据库存储一个长度为800字节的记录时,仍然会给此记录分配长度
为1024字节的空间,存储1025字节的记录时,则会分配2048字节的空间。这将有利于在记录的数据长度会不断变化的场合,预先为下次变化保留存储
空间,而合理的提供数据记录块大小值,则能充分的体现这一优化效果。dwChunkSize在数据库创建的时候既被指定,并且此后不得再改变。
ps:
角色描述数据的数据块单位大小是64字节,角色数据的数据块单位大小是2048字节,公会数据的数据块单位大小是1024字节。
一个数据块索引标识一个数据块。
内存中的数据块索引(在索引文件中的记录块描述):
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;//本索引记录在索引文件中的偏移位置 };
将大的数据块拆分为小数据块的条件为:
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; }
在文件中分配一个新的数据块。
获取数据块:
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; //空闲数据偏移排序表,用于合并连续的空数据位置 //查找方式也是有排序的就使用折半查找,否则就从后往前一个个查找 INT_PTR nOffsetIndex = m_FreeDataOffsetList.search(pResult->chunk.nDataOffset); if (nOffsetIndex > -1) { //找到 记录在数据库文件中的绝对偏移值 跟m_FreeDataSizeList中的索引一样的就删除m_FreeDataOffsetList中的这个数据块 Assert(m_FreeDataOffsetList[nOffsetIndex].pIndex == pResult); //分别从空闲偏移以及空闲大小列表中删除索引记录 m_FreeDataSizeList.remove(nIdx); m_FreeDataOffsetList.remove(nOffsetIndex); } else { logError( _T("Missing SizeRecord of Free ChunkIndex at %08X"), pResult); Assert(FALSE); pResult = NULL; } } } //空闲数据块列表中没有合适的记录,则在数据文件末尾写入 if (!pResult) { if (m_NullIndexList.count() > 0) { pResult = m_NullIndexList.pop();//使用了内存池来管理多余的内存 } else { pResult = m_IndexAllocator.allocObjects(1);//如果空索引内存池也没有就使用分配器来分配一个新的,并添加到索引记录列表 m_IndexRecordList.add(pResult);//m_IndexRecordList用来记录分配的记录索引的数量 //将nIndexOffset置为-1,以便在写索引的时候再确定记录偏移位置 pResult->nIndexOffset = -1; } //将nDataOffset置为-1,以便在写数据的时候再确定偏移位置 //无论是在m_NullIndexList列表中的索引记录或新创建的索引记录,都是没有数据块的! //就是个没有指向数据的块索引 pResult->chunk.nDataOffset = -1; } //填充新索引记录的所有有效字段 pResult->chunk.nId = nId; pResult->chunk.nDataSize = dwDataSize; pResult->chunk.nChunkSize = nChunkSize; return pResult; }
当一个数据块没有足够的空间时,就需要重新分配空间,以获取一个空间合适的数据库,返回该数据块的索引。
重新分配方式:
CCustomFileDB::ChunkIndex* CCustomFileDB::reallocChunk(ChunkIndex *pIndex, INT64 dwNewDataSize) { INT64 nMergedChunkSize; ChunkIndex *pNewIndex; //首先在尝试与该数据块后面相邻的空闲数据块进行合并 INT_PTR nIdx = m_FreeDataOffsetList.search(pIndex->chunk.nDataOffset + pIndex->chunk.nChunkSize); pNewIndex = ((nIdx > -1) ? m_FreeDataOffsetList[nIdx].pIndex : NULL); //如果该数据块与下一个块的总大小可以容纳新的数据长度,则进行合并,否则就申请新的记录块 //新的块和合并的块的索引都需要更新到索引文件 if (pNewIndex && (nMergedChunkSize = pIndex->chunk.nChunkSize + pNewIndex->chunk.nChunkSize) >= dwNewDataSize) { //如果完全合并后的块太大,将从空闲块中拆分一部分空间与当前块合并,剩余的空间继续作为一个独立的块 //例如,我们需要存储10K的数据,而空闲块中有1GB的数据,如果将1G与当前快合并将造成过度的浪费 INT64 nSplitOutSize = getChunkSplitOutSize(nMergedChunkSize, dwNewDataSize);//将合并块(实际上也是空闲块里)中拆分一部分空间出来 if (nSplitOutSize > 0) { Assert(nSplitOutSize <= pNewIndex->chunk.nChunkSize); //减少空闲块的长度 pNewIndex->chunk.nDataOffset += pNewIndex->chunk.nChunkSize - nSplitOutSize; pNewIndex->chunk.nChunkSize = nSplitOutSize; if (!flushIndex(pNewIndex))//更新原来索引到索引文件 return NULL; //增加当前块的长度 pIndex->chunk.nChunkSize = nMergedChunkSize - nSplitOutSize; pIndex->chunk.nDataSize = dwNewDataSize; if (!flushIndex(pIndex))//更新空闲块索引到索引文件 return NULL; } else//后面一块(空闲块)被完全合并到前一块 { //将空闲数据块调整为无效数据块 pNewIndex->chunk.nChunkSize = 0;//尽管索引文件里还是有无效索引记录,但数据文件中是没有无效索引数据块的,目前无效索引是作为索引对象分配而存在于无效索引列表 if (!flushIndex(pNewIndex)) return NULL; //扩展当前块的长度 pIndex->chunk.nChunkSize = nMergedChunkSize; pIndex->chunk.nDataSize = dwNewDataSize; if (!flushIndex(pIndex))//更新当前块索引到索引文件 return NULL; //将空闲块索引记录从空闲偏移以及大小列表中移除 INT_PTR nSizeListIdx = getFreeSizeListIndex(pNewIndex); Assert(nSizeListIdx > -1); m_FreeDataSizeList.remove(nSizeListIdx); m_FreeDataOffsetList.remove(nIdx); //将无效块索引记录添加到无效块索引列表 m_NullIndexList.add(pNewIndex); } return pIndex; } else { //申请新的记录块,并向新的块中写入数据 pNewIndex = allocChunk(pIndex->chunk.nId, dwNewDataSize); //向索引文件中更新旧索引 pIndex->chunk.nId = 0; if (!flushIndex(pIndex)) return NULL; //将旧的索引记录添加到空闲记录列表中 addFreeChunk(pIndex); return pNewIndex; } return NULL; }
参数dwSplitOutSize为从大数据块中拆分出来的新数据块的大小
bool CCustomFileDB::splitLargeChunk(ChunkIndex *pIndex, INT64 dwSplitOutSize) { //减少当前数据块的大小并写入索引文件中 Assert(pIndex->chunk.nChunkSize >= dwSplitOutSize); pIndex->chunk.nChunkSize -= dwSplitOutSize; if (!flushIndex(pIndex)) return false; //申请新的记录块,并向将剩余空间保留给新记录块 ChunkIndex *pNewIndex = allocChunk(0, 0); pNewIndex->chunk.nChunkSize = dwSplitOutSize; pNewIndex->chunk.nDataOffset= pIndex->chunk.nDataOffset + pIndex->chunk.nChunkSize; //将新的索引记录添加到空闲记录列表中 addFreeChunk(pNewIndex); //新索引写入到索引文件中 if (!flushIndex(pNewIndex)) return false; return true; }
空闲块管理列表分为:
1)空闲数据大小排序表
空闲数据大小排序表用于快速查询适合大小的空闲块。
2) 空闲数据偏移排序表
空闲数据偏移排序表用于查询可以合并的空闲模块(只有在数据文件中偏移是连续的块才符合可合并的标准)。
空闲数据大小排序表,用于快速找到一个合适的空数据位置(查找到合适的数据块索引(ChunkIndex),然后找到合适的文件位置)
FreeDataSizeList m_FreeDataSizeList;
其中,FreeDataSizeList是以FreeDataSizeIndex 为成员类型的列表。
空闲记录块大小排序列表中的成员 ,比较方式:
1)比较两个数据块索引的数据块大小
2)比较一个数据块索引的数据块大小和一个具体值
struct FreeDataSizeIndex { ChunkIndex *pIndex; public: inline operator ChunkIndex* (){ return pIndex; } inline bool operator == (const FreeDataSizeIndex & another) const { return pIndex == another.pIndex; } inline INT_PTR compare (const FreeDataSizeIndex & another) const { //if (pRecord == another.pRecord) return 0; if (this->pIndex->chunk.nChunkSize < another.pIndex->chunk.nChunkSize) return -1; else if (this->pIndex->chunk.nChunkSize > another.pIndex->chunk.nChunkSize) return 1; else return 0; } inline INT_PTR compareKey(const INT64 nSize) const { if (this->pIndex->chunk.nChunkSize < nSize) return -1; else if (this->pIndex->chunk.nChunkSize > nSize) return 1; else return 0; } };
空闲数据偏移排序表,用于合并连续的空数据位置(查找到合适的ChunkIndex来合并,也会合并索引文件和数据文件中对应的项)
FreeDataOffsetList m_FreeDataOffsetList;
FreeDataOffsetList是以FreeDataOffsetIndex为成员类型的列表。
空闲记录块偏移排序列表的成员,比较方式:
1)比较两个数据块索引的数据块绝对偏移(在数据文件)
2)比较一个数据块索引的数据块绝对偏移(在数据文件)和一个具体值
struct FreeDataOffsetIndex { ChunkIndex *pIndex; public: inline operator ChunkIndex* (){ return pIndex; } inline bool operator == (const FreeDataOffsetIndex & another) const { return pIndex == another.pIndex; } inline INT_PTR compare (const FreeDataOffsetIndex & another) const { if (this->pIndex->chunk.nDataOffset < another.pIndex->chunk.nDataOffset) return -1; else if (this->pIndex->chunk.nDataOffset > another.pIndex->chunk.nDataOffset) return 1; //对于存储数据块索引的列表,如果出现相同偏移位置的索引的情况,那么就是发生错误了! else { Assert(pIndex == another.pIndex); return 0; }; } inline INT_PTR compareKey(const INT64 nOffset) const { if (this->pIndex->chunk.nDataOffset < nOffset) return -1; else if (this->pIndex->chunk.nDataOffset > nOffset) return 1; else return 0; } };
将新的索引记录添加到空闲记录列表:
1)空闲数据偏移排序表
2)空闲数据大小排序表
步骤:
1)刷新数据文件中的对应的索引记录的无效标识
2)把该数据块跟前后相邻的空闲数据块合并成一个数据块
3)更新空闲数据偏移排序表和空闲数据大小排序表(有数据块合并)
4)写入数据块索引到索引文件(有数据块合并)
void CCustomFileDB::addFreeChunk(ChunkIndex *pIndex) { ChunkIndex *pExistsRecord; FreeDataOffsetIndex offsetIndex; FreeDataSizeIndex sizeIndex; INT_PTR nIdx, nListIndex, nMergeFrom, nMergedCount, nListCount; DataHeader dh; Assert(pIndex->chunk.nId == 0); Assert(pIndex->chunk.nDataOffset != -1); //将原有的数据头标记为无效数据,以防在通过数据文件重建索引时将无用的数据块再挖掘出来! dh.dwIdent = DataHeader::INVALID_IDENT;//标记数据无效 writeDataFile(pIndex->chunk.nDataOffset, &dh.dwIdent, sizeof(dh.dwIdent)); //将索引记录添加到数据块偏移以及大小排序列表中 offsetIndex.pIndex = pIndex; sizeIndex.pIndex = pIndex; nIdx = m_FreeDataOffsetList.add(offsetIndex); m_FreeDataSizeList.add(sizeIndex); //尝试合并与改数据块前后相邻的空闲数据块 nMergeFrom = -1; nMergedCount = 0; nListIndex = nIdx - 1; //如果新加的块的数据偏移正好是前面一个块的块大小的边缘 //********向前合并连续的数据块为一个大块 while (nListIndex >= 0) { pExistsRecord = m_FreeDataOffsetList[nListIndex].pIndex; if (pExistsRecord->chunk.nDataOffset + pExistsRecord->chunk.nChunkSize == pIndex->chunk.nDataOffset) { pExistsRecord->chunk.nChunkSize += pIndex->chunk.nChunkSize; pIndex->chunk.nChunkSize = 0; pIndex->chunk.nId = 0; pIndex = pExistsRecord; nMergeFrom = nListIndex; nMergedCount++; nListIndex--; } else break; } //如果新加的块的数据偏移正好是后面一个块的块大小的边缘 //********向后合并连续的数据块为一个大块 nListCount = m_FreeDataOffsetList.count(); nListIndex = nIdx + 1; while (nListIndex < nListCount) { pExistsRecord = m_FreeDataOffsetList[nListIndex].pIndex; if (pIndex->chunk.nDataOffset + pIndex->chunk.nChunkSize == pExistsRecord->chunk.nDataOffset) { pIndex->chunk.nChunkSize += pExistsRecord->chunk.nChunkSize; pExistsRecord->chunk.nChunkSize = 0; pExistsRecord->chunk.nId = 0; if (nMergeFrom == -1) nMergeFrom = nIdx; nMergedCount++; nListIndex++; } else break; } if (nMergedCount > 0) { INT_PTR i; //********将被合并的数据块的索引从块偏移以及块大小列表中移除 for (i=nMergeFrom + nMergedCount; i> nMergeFrom; --i) { pIndex = m_FreeDataOffsetList[i].pIndex; //目前查找pIndex在m_FreeDataSizeList中索引的操作采取的是完全遍历的方式! //由于m_FreeDataSizeList使用数据块大小成员进行排序,而列表中可能存在多个大 //小相同的块的索引记录,因而如果使用基于对比的快速查找有可能返回其他的指针的问题。 nListIndex = getFreeSizeListIndex(pIndex); if (nListIndex > -1) { m_FreeDataSizeList.remove(nListIndex); //合并后减少数据空闲块 m_NullIndexList.add(pIndex); flushIndex(pIndex);//更新无效索引到索引文件 } else Assert(FALSE); } m_FreeDataOffsetList.remove(nMergeFrom + 1, nMergedCount);//合并后减少数据空闲块 //向索引文件中写入被合并后的最终块索引 pIndex = m_FreeDataOffsetList[nMergeFrom].pIndex; flushIndex(pIndex); } validateListCount(); }
CLocalDB文件数据库对象本身是个线程类对象,在其启动初始化时需要设置文件路径和数据块单位大小,读取所有索引文件到索引列表,加载角色描述到角色描述列表。
文件包括:
1)角色描述数据库文件(索引文件和数据文件)
2)角色数据数据库文件(索引文件和数据文件)
3)帮会数据数据库文件(索引文件和数据文件)
BOOL CLocalDB::OpenDB(LPCSTR sDBPath) { static const TCHAR szCharDescDBName[] = _T("CharDesc");//角色描述数据库名 static const TCHAR szCharDataDBName[] = _T("CharData");//角色数据数据库名 static const TCHAR szGuildDataDBName[] = _T("GuildData");//帮会数据数据库名 TCHAR sDBName[1024]; sDBName[ArrayCount(sDBName) - 1] = 0; //关闭当前打开的数据库 CloseDB(); _tcscpy(m_szDBPath, sDBPath); //打开角色描述数据库 _sntprintf(sDBName, ArrayCount(sDBName) - 1, _T("%s/%s"), sDBPath, szCharDescDBName); if (!m_CharDescDB.open(sDBName) && !m_CharDescDB.create(sDBName)) { logError("无法读取或创建角色描述数据库"); return FALSE; } m_CharDescDB.setChunkSize(64); //读取角色描述数据库 if (!LoadAllCharDesc()) return FALSE; //打开角色数据数据库 _sntprintf(sDBName, ArrayCount(sDBName) - 1, _T("%s/%s"), sDBPath, szCharDataDBName); if (!m_CharDataDB.open(sDBName) && !m_CharDataDB.create(sDBName)) { logError("无法读取或创建角色数据存储数据库"); return FALSE; } m_CharDataDB.setChunkSize(2048); //打开帮会数据数据库 _sntprintf(sDBName, ArrayCount(sDBName) - 1, _T("%s/%s"), sDBPath, szGuildDataDBName); if (!m_GuildDataDB.open(sDBName) && !m_GuildDataDB.create(sDBName)) { logError("无法读取或创建角色数据存储数据库"); return FALSE; } m_GuildDataDB.setChunkSize(1024); m_boOpened = TRUE; return TRUE; }
数据块索引类型(根据索引中的数据块描述信息):
1)有效数据块索引(nChunkSize>0)
2)无效索引(nChunkSize == 0)
3)空闲数据块索引(nId == 0)
bool CCustomFileDB::open(LPCTSTR sDBName) { CFDBOpenHelper openHelper; //以共享读方式打开数据文件 MnString sPath = sDBName; MnString sFilePath = sPath + DataFileExt; openHelper.m_dataFd = ::open(sFilePath, O_RDWR, 0); if (-1 == openHelper.m_dataFd) { OutputError(GetLastError(), _T("Can not open DataFile \"%s\""), sFilePath.rawStr()); return false; } //以共享读方式打开索引文件 sFilePath = sPath + IndexFileExt; openHelper.m_indexFd = ::open(sFilePath, O_RDWR, 0); if (-1 == openHelper.m_indexFd) { OutputError(GetLastError(), _T("Can not open IndexFile \"%s\""), sFilePath.rawStr()); return false; } //读取文件头 FDBHeader hdr; int dwBytesReaded; dwBytesReaded = ::read(openHelper.m_indexFd, &hdr, sizeof(hdr)); if (0 == dwBytesReaded || dwBytesReaded != (int)sizeof(hdr)) { OutputError(GetLastError(), _T("Can not read IndexHeader")); return false; } //检查文件头标志 if (hdr.dwIdent != FDBHeader::IDENT) { logError( _T("Invalid IndexHeader %08X"), hdr.dwIdent); return false; } //检查文件版本 if (hdr.dwVersion != FDBHeader::VERSION) { logError( _T("Invalid IndexVersion %08X"), hdr.dwVersion); return false; } //申请索引数据临时内容缓冲区 int dwDataIndexSize = sizeof(openHelper.pChunkDescBuffer[0]) * hdr.nRecordCount; openHelper.pChunkDescBuffer = (ChunkIndex::ChunkDesc*)malloc(dwDataIndexSize); if (!openHelper.pChunkDescBuffer) { logError( _T("Out of memory to alloc indexbuffer %08X"), hdr.dwVersion); return false; } //读取索引文件数据内容 dwBytesReaded = ::read(openHelper.m_indexFd, openHelper.pChunkDescBuffer, dwDataIndexSize); if (-1 == dwBytesReaded || dwBytesReaded != dwDataIndexSize) { logError( _T("Invalid IndexVersion %08X"), hdr.dwVersion); return false; } //关闭当前文件 close(); //将文件句柄以及文件头保存到文件对象中 m_indexFd = openHelper.m_indexFd; m_dataFd = openHelper.m_dataFd; m_nNextIndexOffset = sizeof(m_Header) + dwDataIndexSize; //记录索引文件最后位置 openHelper.m_indexFd = openHelper.m_dataFd = -1; m_Header = hdr; //保留读取内存空间 m_IndexRecordList.reserve(hdr.nRecordCount); m_DataList.setSorted(FALSE); m_DataList.reserve(hdr.nRecordCount); m_FreeDataSizeList.setSorted(FALSE); m_FreeDataSizeList.reserve(hdr.nRecordCount); m_FreeDataOffsetList.setSorted(FALSE); m_FreeDataOffsetList.reserve(hdr.nRecordCount); m_NullIndexList.reserve(hdr.nRecordCount); //开始读取索引数据到内存 ChunkIndex::ChunkDesc *pChunkDesc = openHelper.pChunkDescBuffer; ChunkIndex *pIndex = m_IndexAllocator.allocObjects(hdr.nRecordCount); AvaliableDataIndex avalIndex; FreeDataSizeIndex sizeIndex; FreeDataOffsetIndex offsetIndex; INT64 nOffset = sizeof(hdr); for (INT_PTR i=hdr.nRecordCount-1; i>-1; --i) { pIndex->chunk = *pChunkDesc; pIndex->nIndexOffset = nOffset; m_IndexRecordList.add(pIndex); //如果记录块大小值为0则表示该记录为没有任何意义,需将记录存储在空闲块列表中。 if (pChunkDesc->nChunkSize == 0) { m_NullIndexList.add(pIndex); } //如果记录ID值为零则表示该记录为一个空闲的数据块,需将记录存储在空闲块列表中。 else if (pChunkDesc->nId == 0) { sizeIndex.pIndex = pIndex; m_FreeDataSizeList.add(sizeIndex); offsetIndex.pIndex = pIndex; m_FreeDataOffsetList.add(offsetIndex); } else { //数据有效,添加到有效数据列表中 avalIndex.pIndex = pIndex; m_DataList.add(avalIndex); } nOffset += sizeof(*pChunkDesc); pChunkDesc++; pIndex++; } m_DataList.setSorted(TRUE);//有效数据块的索引列表 排序 m_FreeDataSizeList.setSorted(TRUE);//空闲数据块的数据大小索引列表 排序 m_FreeDataOffsetList.setSorted(TRUE);//空闲数据块的偏移大小索引列表 排序 return true; }
读取本地角色描述数据文件到角色描述数据列表。
参考:http://blog.csdn.net/chenjiayi_yun/article/details/37834121
根据数据索引、数据头和数据体把数据写到数据文件。
写入内容:
1)数据记录头
2)数据记录体
索引是记录数据块的偏移信息。
bool CCustomFileDB::writeData(ChunkIndex *pIndex, const DataHeader *pDataHeader, LPCVOID lpDataBuffer) { INT64 nOffset = pIndex->chunk.nDataOffset;//数据记录在数据文件的偏移位置 LONG lFileSize, lNewSize; INT64 nBlockDataSize = pDataHeader->nDataSize + sizeof(*pDataHeader);//数据记录长度+数据记录头长度 == 数据块有效长度 bool boIsNewData = nOffset == -1;//判断是否是新的数据块,是的话就写到数据文件末尾 Assert(pIndex->chunk.nId == pDataHeader->nId);//索引id与数据记录id一致 Assert(pIndex->chunk.nChunkSize >= nBlockDataSize);//文件数据块大小大于等于数据块有效长度 Assert(pIndex->chunk.nChunkSize >= pIndex->chunk.nDataSize);//文件数据块大小大于等于数据块有效长度 CSafeLock sl(&m_DataFileLock); //偏移为-1表示这是一个新的数据块,需要写入数据文件末尾 if (boIsNewData) { struct stat fileStat; if (-1 == fstat(m_dataFd, &fileStat)) { OutputError(errno, _T("Can not query data file size")); return false; } pIndex->chunk.nDataOffset = nOffset = fileStat.st_size;//获取文件大小(末尾的位置)作为新的数据块的偏移位置 } //写入数据块的数据记录头 //每个数据块均以一个数据记录头开始 if (!writeDataFile(nOffset, pDataHeader, sizeof(*pDataHeader)))//数据块的数据头 return false; nOffset += sizeof(*pDataHeader); if (!writeDataFile(nOffset, lpDataBuffer, pDataHeader->nDataSize))//写入数据体 return false; //如果是新数据块且数据库头中指定了数据块单元大小,则调整数据文件以进行数据块大小对齐 if (boIsNewData && m_Header.dwChunkSize > 0 && pIndex->chunk.nChunkSize != nBlockDataSize) { lFileSize = pIndex->chunk.nDataOffset + pIndex->chunk.nChunkSize;//新的文件大小 == 新记录在数据库文件中的绝对偏移值 + 新数据记录块大小 //调整文件指针到新数据块大小单元结束处 lNewSize = lseek(m_dataFd, lFileSize, SEEK_SET); if(0 >= lNewSize) { OutputError(errno, _T("Can not query data file size")); return false; } //设置文件结束位置 #ifdef WINDOWS if (!SetEndOfFile(m_pDataFile)) #else if(-1 == ftruncate(m_dataFd, lFileSize)) #endif { OutputError(errno, _T("Can not set end of data file")); return false; } } return true; }
bool CCustomFileDB::writeDataFile(INT64 nOffset, LPCVOID lpBuffer, INT64 dwSize) { static const DWORD OnceWriteBytes = 0x10000;//每次写文件的字节数 CSafeLock sl(&m_DataFileLock); if(-1 != lseek(m_dataFd, (long)nOffset, SEEK_SET)) { OutputError(errno, _T("Fatal error can not set data file pointer")); return false; } size_t dwBytesToWrite, dwBytesWriten; const char* ptr = (const char*)lpBuffer; while (dwSize > 0) { if (dwSize > OnceWriteBytes) dwBytesToWrite = OnceWriteBytes; else dwBytesToWrite = (LONG)dwSize; dwBytesWriten = ::write(m_dataFd, ptr, dwBytesToWrite); if (0 >= dwBytesWriten) { OutputError(errno, _T("Fatal error can not write data file")); return false; } ptr += dwBytesWriten; dwSize -= dwBytesWriten; } if (m_boFlushFileDataImmediately) { fsync(m_dataFd); } return true; }
写到索引文件内容:
1)写入索引记录的数据描述信息
2)更新索引文件的数据头中的数量信息(若是新索引)
bool CCustomFileDB::flushIndex(ChunkIndex *pIndex) { bool isNewIndex = pIndex->nIndexOffset == -1; //如果是新索引,则计算并确定索引偏移 if (isNewIndex) { pIndex->nIndexOffset = m_nNextIndexOffset; //TRACE(_T("New IndexOffset is : %08X\r\n"), pIndex->nIndexOffset); m_nNextIndexOffset += sizeof(pIndex->chunk); } //向索引文件中写入索引记录 if (!writeIndexFile(pIndex->nIndexOffset, &pIndex->chunk, sizeof(pIndex->chunk))) return false; //如果是新索引,则更新索引文件头 if (isNewIndex) { m_Header.nRecordCount++; if (!writeIndexFile(0, &m_Header, sizeof(m_Header))) return false; } return true; }
bool CCustomFileDB::writeIndexFile(INT64 nOffset, LPCVOID lpBuffer, INT64 dwSize) { static const DWORD OnceWriteBytes = 8192;//每次写文件的字节数 CSafeLock sl(&m_IndexFileLock); if(-1 == lseek(m_indexFd, (LONG)nOffset, SEEK_SET)) { OutputError(errno, _T("Fatal error can not set index file pointer")); return false; } DWORD dwBytesToWrite = 0; int nBytesWriten; const char* ptr = (const char*)lpBuffer; while (dwSize > 0) { if (dwSize > OnceWriteBytes) dwBytesToWrite = OnceWriteBytes; else dwBytesToWrite = (LONG)dwSize; nBytesWriten = ::write(m_indexFd, ptr, dwBytesToWrite); if (-1 == nBytesWriten) { OutputError(errno, _T("Fatal error can not write index file")); return false; } ptr += nBytesWriten; dwSize -= nBytesWriten; } if (m_boFlushFileDataImmediately) { fsync(m_indexFd); } return true; }
数据的查询(处理逻辑服务器发来的),这里是以查询角色数据为例:
1)逻辑服务器发来的数据查询请求消息,会提交到本地数据文件存取线程的角色数据查询请求队列(网络数据包派送是另一个队列)。
2)检查角色描述列表的是否有该角色,并且检查该角色的状态
3)从更新队列里面取角色数据,防止出现快速重登录导致的回档现象(并且需要是从后往前找的 ,不然会出现回档现象)
4)角色数据更新列表里没有,就检查角色有效数据索引列表。先检查有效数据索引列表有没有该角色id,有就从数据文件中读取数据。
VOID CDBDataClient::CatchLoadCharData(CDataPacketReader &inPacket) { INT nUserId; INT64 nCharId; DWORD dwDataSize; inPacket >> nUserId; inPacket >> nCharId; CDataPacket &pack = AllocProtoPacket(DBType::dsLoadCharData); //先检查角色描述列表的是否有该角色,并且检查该角色的状态 CharDesc *pCharDesc = m_pLocalDB->GetCharDesc(nCharId); if (!pCharDesc || pCharDesc->nUserId != nUserId || (pCharDesc->wState & (CHARSTATE_DELETED | CHARSTATE_DISABLED))//检查角色是否被删除或者被禁止 || pCharDesc->wServerId != m_nServerId ) { //Error: 非法的角色数据加载请求 pack << (int)1; pack << nCharId; } else { dwDataSize = 0; pack << (int)0;//SUCCESS pack << nCharId; pack << dwDataSize; //优先从更新队列里面取角色数据,防止出现快速重登录导致的回档现象 dwDataSize = m_pLocalDB->GetCharDataInUpdateList(nCharId, pack); if (dwDataSize != 0) { pack.adjustOffset(-(INT64)(dwDataSize + sizeof(dwDataSize))); pack << dwDataSize;//写入数据长度 pack.adjustOffset(dwDataSize); } else //角色数据更新列表里没有,就检查角色有效数据索引列表 { //从磁盘文件读取 dwDataSize = (DWORD)m_pLocalDB->GetCharData(nCharId, NULL, 0);//先检查有效数据索引列表有没有该角色id,有就数据文件中读取数据 pack.adjustOffset(-(INT64)(sizeof(dwDataSize))); pack << dwDataSize;//写入数据长度(包括数据头和数据体) //如果有角色数据则直接返回角色数据,否则将返回角色描述 if (dwDataSize > 0)//数据记录块,除了数据头,还有数据体 { pack.reserve(pack.getLength() + dwDataSize); pack.adjustOffset(m_pLocalDB->GetCharData(nCharId, pack.getOffsetPtr(), dwDataSize)); } else { //角色数据描述信息是保存在内存的基于角色ID排序的角色描述列表 //返回角色描述到数据包 WriteCharDesc(pack, pCharDesc); } } } FlushProtoPacket(pack);//提交到发送队列 }
在等待写入磁盘中的队列数据中获取角色数据,防止客户端快速重
新登陆时因队列中的数据尚未保存而直接获取老的磁盘文件数据而产生的回档现象。
返回 0表示队列中没有数据
DWORD CLocalDB::GetCharDataInUpdateList(INT64 nCharId, CDataPacket& pack) { INT_PTR nListCount = 0; DWORD dwDataSize = 0; CGameDataBufferReader *pGDBReader; m_CharDataUpdateList.lock(); //必须加锁,否则得到的指针很可能会因为写数据的线程操作移除后而失效!(逻辑线程与网络线程之间的数据交换的互斥) m_CharDataUpdateList.flush(); //将附加队列进行整合,然后统一查找角色ID nListCount = m_CharDataUpdateList.count(); for (INT_PTR i=nListCount-1; i>-1; --i) //从后往前找才会遇到最新的数据 { pGDBReader = m_CharDataUpdateList[i]; if (nCharId == pGDBReader->GetDataId()) { //填充角色数据到发送包 dwDataSize = (DWORD)pGDBReader->getLength(); pack.reserve(pack.getLength() + dwDataSize); pack.writeBuf(pGDBReader->getMemoryPtr(), dwDataSize);//写入二进制数据 break; } } m_CharDataUpdateList.unlock(); return dwDataSize; }
读取文件数据:
1)根据有效索引的数据id获取数据索引,再获取数据的长度
2)根据索引指向的数据偏移,获取数据内容(指定数据长度,不可超出该数据块的有效数据长度(数据体长度))
3) 读取数据文件数据到缓存
INT64 CCustomFileDB::get(INT64 nDataId, LPVOID lpBuffer, INT64 dwBufferSize) const { static const DWORD DataHeaderSize = sizeof(DataHeader); INT_PTR nIdx = m_DataList.search(nDataId); if (nIdx < 0) return 0; ChunkIndex *pIndex = m_DataList[nIdx].pIndex; Assert(pIndex->chunk.nDataSize >= DataHeaderSize); //dwBufferSize为0则仅表示获取数据长度 if (dwBufferSize == 0) { if (pIndex->chunk.nDataSize > DataHeaderSize) return pIndex->chunk.nDataSize - DataHeaderSize; else return 0; } else { INT64 nOffset = pIndex->chunk.nDataOffset + DataHeaderSize; INT64 nSize = pIndex->chunk.nDataSize - DataHeaderSize; if (dwBufferSize > nSize) dwBufferSize = nSize; if (!readDataFile(nOffset, lpBuffer, dwBufferSize)) return 0; return dwBufferSize; } }
处理逻辑服务器发来的保存请求,提交到数据更新队列
VOID CDBDataClient::CatchSaveCharData(CDataPacketReader &inPacket) { INT64 nCharId; DWORD dwDataSize; inPacket >> nCharId; inPacket >> dwDataSize; CGameDataBuffer *pBuffer = m_pDataServer->AllocGameDataBuffer(); pBuffer->AddRef(); pBuffer->SetData(nCharId, inPacket.getOffsetPtr(), dwDataSize); CharDesc *pCharDesc = m_pLocalDB->GetCharDesc(nCharId); if (pCharDesc) { //跳过角色基本数据头结构 common::DBType::PlayerBasicData *pBaseData = (common::DBType::PlayerBasicData *)pBuffer->getPositionPtr(sizeof(common::DBType::DataSectionHeader)); //发起对角色描述数据的更新 pCharDesc->wLevel = pBaseData->wLevel; pCharDesc->btGender = pBaseData->btGender; pCharDesc->nUpdateTime = CMiniDateTime::now(); //将角色描述数据添加到到更新请求队列中 m_pLocalDB->PostUpdateCharDesc(pCharDesc);//保存角色数据到本地数据库(派送到文件数据库线程) m_pSQLDB->PostUpdateCharDesc(pCharDesc);//保存角色数据到mysql数据库(派送到mysql线程(用于后台)) } else logError( _T("试图更新在本地角色描述数据中找不到的角色数据(%lld)"), nCharId); //将角色数据添加到到更新请求队列中 m_pLocalDB->PostUpdateCharData(pBuffer);//保存角色数据到本地数据库 m_pSQLDB->PostUpdateCharData(pBuffer);//保存角色数据到mysql数据库 pBuffer->Release(); CDataPacket &pack = AllocProtoPacket(DBType::dsSaveCharData); pack << (int)0;//SUCCESS pack << nCharId; FlushProtoPacket(pack);//返回消息到逻辑服务器 }
本地文件写线程的周期处理,处理数据更新队列的数据:
1)数据更新队列是准备写入数据到数据文件的队列
2)周期循环把处理队列请求写入数据文件和索引文件
VOID CLocalDB::ProcessCharDescUpdate(TICKCOUNT dwTimeLimit) { CharDesc *pCharDesc; TICKCOUNT dwTimeOver = _getTickCount() + dwTimeLimit; INT_PTR nCount = m_CharDescUpdateList.count(); while (nCount > 0 && _getTickCount() < dwTimeOver) { pCharDesc = m_CharDescUpdateList.pop(); //m_CharDescDB 是角色描述数据库,也是数据文件处理对象,文件处理管理类,用于把角色数据写入到角色数据文件 if (!m_CharDescDB.put(pCharDesc->nCharId, pCharDesc, sizeof(*pCharDesc))) { logError("无法向角色描述库更新角色(%lld)的描述信息", pCharDesc->nCharId); } nCount--; } }
写入数据到数据文件和索引文件。
步骤:
1)在有效数据列表里查找数据块的索引
2)在找到有效索引情况下:
如果当前块空间足够容纳新数据,写入数据到该索引指向的数据文件的位置,然后检查是否因当前块的数据减少了从而需要将一个大的块拆解为两个独立的记录块。
如果当前快空间不足容纳新数据,则必须重新申请一个新的块来存放数据并且将当前块添加为空闲块。(原来数据块被重新分配,具体看reallocChunk实现),然后写入数据到该索引指向的数据文件的位置,写入索引到索引文件(索引被改变了)
3)在找不到有效索引情况下:
申请新的记录块,并向新的块中写入数据到数据文件,再写入新块的索引到索引文件
bool CCustomFileDB::put(INT64 nDataId, LPCVOID lpData, INT64 dwSize)//数据id nDataId, 数据缓存地址 lpData, 数据长度 dwSize { //不允许数据ID值为0,因为在索引中记录ID值为零用于表示索引记录包含一个空闲的数据块 if (nDataId == 0) { Assert(FALSE); return false; } ChunkIndex *pIndex, *pNewIndex; DataHeader dh;//数据头 dh.dwIdent = DataHeader::VALID_IDENT; dh.nId = nDataId; dh.nDataSize = dwSize; dwSize += sizeof(dh); INT_PTR nIdx = m_DataList.search(nDataId);//在有效数据列表里查找数据块的索引 if (nIdx > -1) { pIndex = m_DataList[nIdx]; /*如果当前块空间足够容纳新数据,则检查是否因当前块的数据减少了从而需要将一 个大的块拆解为两个独立的记录块。如果当前快空间不足容纳新数据,则必须申请一 个新的块来存放数据并且将当前块添加为空闲块。*/ if (pIndex->chunk.nChunkSize >= dwSize) { INT64 nOldDataSize = pIndex->chunk.nDataSize; pIndex->chunk.nDataSize = dwSize; if (!writeData(pIndex, &dh, lpData)) return false; //计算可以从这个大的数据块中分出多大的新的数据块 INT64 dwSplitOutSize = getChunkSplitOutSize(pIndex->chunk.nChunkSize, dwSize);//分出需要按照分割规则来进行,分出的大小需要是块单位大小的倍数 //如果可以分出新的数据块则拆分数据块 if (dwSplitOutSize > 0) { //减少当前索引的块的大小并申请新的索引对象,并把分出来的大小关联到新的 索引对象 if (!splitLargeChunk(pIndex, dwSplitOutSize)) return false; } else { //如果数据块没有进行拆分则检查是否需要更新改块的索引数据 if (nOldDataSize != dwSize) { if (!flushIndex(pIndex)) return false; } } } else { //重新申请新记录块,并向新的块中写入数据 pNewIndex = reallocChunk(pIndex, dwSize); if (!pNewIndex) return false; //向数据文件中更新数据 if (!writeData(pNewIndex, &dh, lpData)) return false; //将新记录块索引写入到索引文件中。 //必须在writeData发生后才能写,因为当pNewIndex是全新的索引时chunk.nDataOffset为-1 //用于表示在writeData发生时再确定数据块索引(分配的是新块的话,是还没写入数据文件的,所以nDataOffset为-1) if (!flushIndex(pNewIndex)) return false; //将新的记录索引替换到数据索引列表中 m_DataList[nIdx].pIndex = pNewIndex; } } else { //申请新的记录块,并向新的块中写入数据 pNewIndex = allocChunk(nDataId, dwSize); if (!writeData(pNewIndex, &dh, lpData)) return false; //将新记录块索引写入到索引文件中 if (!flushIndex(pNewIndex))//<span style="font-family: Arial, Helvetica, sans-serif;">分配的是新块的话,是还没写入数据文件的,所以nDataOffset为-1,需要先写入数据文件,再写入索引文件</span> return false; //将新索引添加到有效数据列表中 AvaliableDataIndex ChunkIndex; ChunkIndex.pIndex = pNewIndex; m_DataList.add(ChunkIndex); } return true; }
本地数据库存储配置:(设置文件目录,文件备份周期和过期备份文件删除周期)
文件数据库的文件是存储在本地的。在数据服务器配置里,配置在目录"./FDB"下。
有三类数据文件
角色描述数据库名(CharDesc)
角色数据数据库名(CharData)
帮会数据数据库名(GuildData)
会有另外的备份线程对 文件数据库的文件进行备份。
配置在目录./BACKUP,时间为60分钟,超过48小时的备份会被删除。