十八.解决写索引代码报异常问题

张开发
2026/4/21 13:31:20 15 分钟阅读

分享文章

十八.解决写索引代码报异常问题
1.vch字符串无效但是运行后报异常大概是在调用数据库CDB写索引的时候提示非法的字符串吧。我这里不调试了。因为我大概猜到是什么原因。2.检查数据成员因为在构造这个区块索引类的时候new CBlockIndex它里面的一些数据class CBlockIndex { public: const uint256* phashBlock; CBlockIndex* pprev; CBlockIndex* pnext; unsigned int nFile; unsigned int nBlockPos; int nHeight; // block header int nVersion; uint256 hashMerkleRoot; unsigned int nTime; unsigned int nBits; unsigned int nNonce;比如上一个区块索引pprev,下一个区块索引,pnext我们是没有的并且也没处理。具体是哪几个我们还得看一下CDiskBlockIndex:class CDiskBlockIndex : public CBlockIndex { public: uint256 hashPrev; uint256 hashNext;因为CBlockIndex会转换成CDiskBlockIndex所以我们得看CDiskBlockIndex构造函数进行了什么操作以及实例化了哪些成员就可以找出没有被赋值的成员并能引发异常。CDiskBlockIndex本身只有两个成员hashPrev,hashNext也就是说这两个成员需要从指针转换成指向的实值。其它的都是继承自CBlockIndex.但是实际检查了一下逻辑explicit CDiskBlockIndex(CBlockIndex* pindex) : CBlockIndex(*pindex) { hashPrev (pprev ? pprev-GetBlockHash() : 0); hashNext (pnext ? pnext-GetBlockHash() : 0); }如果这两个为空后面构造CDiskBlockIndex是会被直接赋0值的这里的hashPrevhashNext不是指针。赋0值是合法的。还有一个const uint256* phashBlock;序列化的时候它并没有参与所以可以忽略它。而且我检查了一下没看到违规调用就像上面pprev?为假它并不会调用GetBlockHash();因为父类的GetBlockHash直接访问了这个值uint256 GetBlockHash() const { return *phashBlock; }而子类有一个同名的是这样的uint256 GetBlockHash() const { CBlock block; block.nVersion nVersion; block.hashPrevBlock hashPrev; block.hashMerkleRoot hashMerkleRoot; block.nTime nTime; block.nBits nBits; block.nNonce nNonce; return block.GetHash(); }也没看到父类的相关调用子类倒是一个调用但无影响因为它调用的子类的函数bool CTxDB::WriteBlockIndex(const CDiskBlockIndex blockindex) { return Write(make_pair(string(blockindex), blockindex.GetBlockHash()), blockindex); }而且我看了下被序列化的成员没看出有什么问题都是合格的。获得了实值。3.调试问题找不出来只能调试测试了报vch这个vch就是CDataStream内部存储序列化数据的变量。应该还是这个数据不对引起的。我们在异常所在的Write函数第一句加一个断点执行到这里然后按f10,逐句执行。在 int ret pdb-put(GetTxn(), datKey, datValue, (fOverwrite ? 0 : DB_NOOVERWRITE));前也可加上这句printf(ssKey size%zu, ssValue size%zu\n, ssKey.size(), ssValue.size());输出一下键和值数据的大小看哪个大小异常判断一下是键引起的还是值的数据引起。4.传参问题然后在执行的时候发现key的数据竟然是version,那么它被序列化加上前置描述大小的字节大小那应该是8.而value显示 70001它的大小是4这应该是一个无符号的整型占4字节。因为不是字符串不用描述长度所以它前面不用加上长度描述。所以不用占字节。数据大小输出现在问题开始明朗传进来的键值都是错的我们一步一步往上查找。我们来看它的上一级调用bool CTxDB::WriteBlockIndex(const CDiskBlockIndex blockindex) { return Write(make_pair(string(blockindex), blockindex.GetBlockHash()), blockindex); }这里应该不会出错应该传进来的blockindex这边的问题。再往上就是这里了txdb.WriteBlockIndex(CDiskBlockIndex(pindexNew));这里有一个判断点我们要判断是pindexNew数据出错还是CDiskBlockIndex转换出的错。直接在这里加上一个断点我在这里动态查看pindexNew里的数据无异常按F11步进CDiskBlockIndex的构造函数看了下挺简单的几句代码应该不会出错啊。返回上面的make_pair那里加一个断点吧查看blockindex数据有无问题。blockindex数据也无问题问题出在make_pair我感觉有点玄。不太可能上次这个函数我没记错的话都使用过啊而且这个是系统函数而且后面的blockindex是直传的。等我思考一下。为防我动态查看的数据有错我直接用代码来测试bool Write(const K key, const T value, bool fOverwrite true) { if (!pdb) return false; cout value.nTime endl;这里直接是报错的报一大堆错误什么nTime: 不是 CTxIndex 的成员 nTime: 不是 uint256 的成员 “.nTime”的左边必须有类/结构/联合。这里我有点奇怪怎么是CTxIndex先不管这个证明了传进来了数据确实是错误的。跟CDataStream无关。报CTxIndex,是数据本身错误造成它把模板可能的类型都报一遍比如后面的不是uint256的成员)然后我在Write中也调用bool CTxDB::WriteBlockIndex(const CDiskBlockIndex blockindex) { cout blockindex.nTime endl; return Write(make_pair(string(blockindex), blockindex.GetBlockHash()), blockindex); }这里输出的nTime是正常的这就奇怪了怎么就这么简单的一个Write函数跳转数据确出错了呢。好像没有重载函数啊而且有的话我也没在别的重载函数设置断点啊说明函数选择一致的啊。5.暂定结论之前走了一点弯路模板传参可能动态查不到数据或者我查看方式不对导致我误以为传了错误的数据。传参数方面是没问题的因为我现在直接不传了把代码写在上一级函数里如下bool CTxDB::WriteBlockIndex(const CDiskBlockIndex blockindex) { if (!pdb) return false; // 1. 构造 key/value std::pairstd::string, uint256 key(blockindex, blockindex.GetBlockHash()); CDiskBlockIndex value blockindex; // 打印调试信息 std::cout blockindex.GetBlockHash().ToString() endl; std::cout Block nTime: value.nTime std::endl; std::cout Block hashPrev: value.hashPrev.ToString() std::endl; std::cout Block hashNext: value.hashNext.ToString() std::endl; std::cout.flush(); // 2. 序列化 Key CDataStream ssKey(SER_DISK); ssKey.reserve(1000); ssKey key; Dbt datKey(ssKey[0], ssKey.size()); // 3. 序列化 Value CDataStream ssValue(SER_DISK); ssValue.reserve(10000); ssValue value; Dbt datValue(ssValue[0], ssValue.size()); // 4. 写入 Berkeley DB int ret pdb-put(GetTxn(), datKey, datValue, DB_NOOVERWRITE); // 如果不想覆盖可以用 DB_NOOVERWRITE if (ret ! 0) { std::cerr WriteBlockIndex: DB put failed, ret ret std::endl; return false; } return true; //return Write(make_pair(string(blockindex), blockindex.GetBlockHash()), blockindex); }然后还是报一样的错误pdb-put这行错误并且还是vch中的字符串无效。而key这个值几乎就是显式写的数据了我调试后CDataStream序列化key后紧接着vch报无效的字符串。而现在key数据又没问题那问题出在CDataStream这个类上面序列化出错了。几分钟后....这个也是误报是个正常的提示因为实际这里的vch已经是有内容的。只是可能序列的数据不符合字符串的解释比如没有结尾但它又把这个vch当成个字符串所以导致这种误报这个提示可以无视。说明序列化没问题应该就是数据本身操作出错了可能是哪里的语法不对。等我细查一下。结论再次反转(传参我恢复代码到下一级查看vch又变成version。我想可能有两个问题。一个是传参方面。一个是数据库方面我们还是先将代码放在上一级函数中先解决数据库问题。解决好后再放回下一级函数。一个一个解决。6.分化解决现在来进一步锁定问题吧我已经把代码放到上一级函数修改了一下如下bool CTxDB::WriteBlockIndex(const CDiskBlockIndex blockindex) { if (!pdb) return false; // 直接在这里构造不经过 Write() 函数 CDataStream ssKey(SER_DISK, 0); // 明确加上版本号 0早期常用 ssKey.reserve(100); ssKey make_pair(string(blockindex), blockindex.GetBlockHash()); CDataStream ssValue(SER_DISK, 0); ssValue.reserve(10000); ssValue blockindex; Dbt datKey(ssKey[0], ssKey.size()); Dbt datValue(ssValue[0], ssValue.size()); int ret pdb-put(GetTxn(), datKey, datValue, 0); // 0 允许覆盖 // 清内存可选 memset(datKey.get_data(), 0, datKey.get_size()); memset(datValue.get_data(), 0, datValue.get_size()); if (ret ! 0) printf(WriteBlockIndex failed! ret%d\n, ret); return (ret 0); //return Write(make_pair(string(blockindex), blockindex.GetBlockHash()), blockindex); }首先这里是put报错我们来判断是序列化后的sskey和ssvalue不符合要求还是put调用本身报错。这里有一种很简单的区分方法那就是直接舍弃put以上所有的代码。7.区分数据还是put问题我们用一种没有问题的构建datkey和datvalue的方法最小化测试如下bool CTxDB::WriteBlockIndex(const CDiskBlockIndex blockindex) { if (!pdb) { printf(ERROR: pdb is NULL\n); return false; } printf( WriteBlockIndex 极简测试开始 \n); printf(block hash %s\n, blockindex.GetBlockHash().ToString().c_str()); // 极简测试存一个最简单的字符串键值对 const char* simpleKey test_blockindex_key; const char* simpleValue this is a simple test value for blockindex; Dbt datKey; datKey.set_data((void*)simpleKey); datKey.set_size(strlen(simpleKey)); Dbt datValue; datValue.set_data((void*)simpleValue); datValue.set_size(strlen(simpleValue)); printf(简单 key 长度 %d, value 长度 %d\n, datKey.get_size(), datValue.get_size()); int ret pdb-put(GetTxn(), datKey, datValue, 0); // 0 允许覆盖 printf(pdb-put 返回值 ret %d\n, ret); if (ret ! 0) { printf(!!! put 失败Berkeley DB 错误码 %d\n, ret); // 常见错误码含义 // 0 成功 // -30996 DB_KEYEXIST (键已存在且没用 DB_NOOVERWRITE) // -30987 EINVAL (参数无效) // -30991 DB_RUNRECOVERY (数据库损坏需要恢复) } else { printf(极简 put 测试成功\n); } // return Write(make_pair(string(blockindex), blockindex.GetBlockHash()), blockindex); return (ret 0); }8.锁定事务这样的数据调用put还是报错异常问题出在数据库操作上面。如果在数据操作上面那之前我们写过数据的没问题只是没有加入事务这个功能。所以问题很有可能出在事务上。我新建了一个干净的项目然后调用事务写数据#define _CRT_SECURE_NO_WARNINGS //设置不报安全警告 #include db_cxx.h #include direct.h #include cstdio #include cstring #include string DbEnv dbenv(0); Db* pdb nullptr; int main() { int ret; printf( Berkeley DB 4.8 事务极简测试 (C 接口) \n); std::string strAppDir c:\\test\\btc; std::string strLogDir strAppDir \\database; _mkdir(strLogDir.c_str()); dbenv.set_lg_dir(strLogDir.c_str()); dbenv.set_lg_max(10000000); dbenv.set_lk_max_locks(10000); dbenv.set_lk_max_objects(10000); dbenv.set_errfile(fopen(db.log, a)); ret dbenv.open(strAppDir.c_str(), DB_CREATE | DB_INIT_LOCK | DB_INIT_LOG | DB_INIT_MPOOL | DB_INIT_TXN | DB_THREAD | DB_PRIVATE | DB_RECOVER, 0); if (ret ! 0) { printf(dbenv.open 失败! ret%d %s\n, ret, db_strerror(ret)); return 1; } printf(dbenv.open 成功\n); // 打开数据库 pdb new Db(dbenv, 0); ret pdb-open(NULL, blkindex.dat, main, DB_BTREE, DB_CREATE | DB_AUTO_COMMIT | DB_THREAD, 0); if (ret ! 0) { printf(pdb-open 失败! ret%d %s\n, ret, db_strerror(ret)); return 1; } printf(数据库打开成功\n); // 事务测试 DbTxn* txn nullptr; // ← 这里必须用 DbTxn* ret dbenv.txn_begin(NULL, txn, 0); // 注意是 dbenv.txn_begin if (ret ! 0) { printf(txn_begin 失败! ret%d %s\n, ret, db_strerror(ret)); return 1; } printf(txn_begin 成功\n); const char* keyStr test_key_001; const char* valueStr this is a simple transaction test value; Dbt datKey((void*)keyStr, (u_int32_t)strlen(keyStr)); Dbt datValue((void*)valueStr, (u_int32_t)strlen(valueStr)); ret pdb-put(txn, datKey, datValue, 0); printf(pdb-put 返回值 %d\n, ret); if (ret ! 0) { printf(put 失败: %s\n, db_strerror(ret)); } else { printf(put 成功\n); } // 提交事务 if (txn) { ret txn-commit(0); printf(txn-commit 返回值 %d\n, ret); } // 清理 if (pdb) { pdb-close(0); // delete pdb; } dbenv.close(0); printf(测试结束\n); return 0; }这个测试结果是没问题的。9.锁定事务写法那么这里的事务没问题说明不是版本兼容问题导致事务无法使用而是我们的写法有问题。我们来仔细看一下事务相关的代码10.事务代码分析从TxnBegin开始public: bool TxnBegin() { if (!pdb) return false; DbTxn* ptxn NULL; int ret dbenv.txn_begin(GetTxn(), ptxn, 0); if (!ptxn || ret ! 0) return false; vTxn.push_back(ptxn); return true; }GetTxn()DbTxn* GetTxn() { if (!vTxn.empty()) return vTxn.back(); else return NULL; }vTxnclass CDB { protected: Db* pdb; string strFile; vectorDbTxn* vTxn;11.txn_begin这个函数是创建一个事务我们来看一下这个函数的三个参数是干什么的第二个参数是用来接收事务第一个参数是指明这个创建事务的父事务如果没有就填null那么这个创建的事务就为顶级事务。第三个参数通常传0就行表示使用默认行为。那么为什么要建立这种父子关系的事务呢如果创建的是子事务好处是子事务可以独立回滚但必须在父事务提交之前提交。但是这个使用有着严格的要求每次用dbenv创建一个子事务必须配合调用TxnCommit提交或TxnAbort来结束这个子事务。才能调用父事务的提交或回滚。或者你子事务没结束又再次dbenv.TxnBegin,这样会造成源码中事务的累积。我们可以看到源码中利用 vectorDbTxn* vTxn;事务数组来存储这种事务的累积。其中自写的TxBegin,GetTxn,TxnCommit,TxnAbort管理着这些逻辑。我们可以看到上面是源码的逻辑而库的逻辑提交和回滚是直接事务调用的如下语句vTxn.back()-commit(0); vTxn.back()-abort();如果你上面有个父事务你也必须也做选择提交还是回滚然后父事务才能做选择如果父事务先处理那么是不行的。错误会发生。另外如果子事务选择提交但最终父事务选择回滚那么其下所有的子事务的操作也是无效的会加滚。这种父子事务是为了保证数据的一致性在某情况是需要的。好理解了上面这些现在我们锁定了就是事务引起的问题又排除了版本的原因。单独测试的代码是没问题的那么问题肯定是出在我们项目中事务的操作代码上。我们需要一句一句来对比项目上的流程跟单独的代码哪里不一样应该就能找出问题所在。我们先来看项目的逻辑先创建事务第一个参数要为null也就是建立顶级事务这一种也是跟单独测试的代码一致我们来设置断点实时查看它是否为空也就是第一次GetTxn是否获取到null。断点设置在这里bool TxnBegin() { if (!pdb) return false; DbTxn* ptxn NULL; DbTxn* p GetTxn();//加上这句设置断点执行后查看是否为null int ret dbenv.txn_begin(GetTxn(), ptxn, 0); if (!ptxn || ret ! 0) return false; vTxn.push_back(ptxn); return true; }结果我就说一下不贴图了为null这一步是正常的。然后再看其下的vTxn.push_back(ptxn); 是否会被执行。没有返回falsevTxn.push_back(ptxn);被执行了也就是说这个事务指针顺利被添加进了vTxn里面也就是 vectorDbTxn* vTxn;父类CDB里的这个成员里。12.GetTxn我们一步一步来不能略每一步我发现在这里设置断点DbTxn* GetTxn() { if (!vTxn.empty()) return vTxn.back(); else return NULL; }我发现vTxn竟然为null也就是这里返回的是为空。并没有返回我们之前添加的事务。但不可能啊如果是这个问题我之前也检测过事务。怎么会通过检测。DbTxn* ptxn GetTxn(); if (ptxn NULL) { printf(ERROR: GetTxn() NULL\n); return false; }就是在WriteBlockIndex函数里然后我调试F11步进函数里发现这次又不为空了。我想了一下GetTxn在哪里被调用了两次然后这是个乌龙第一次创建事务时是会调用一次GetTxn的所以那次为NULL是正常的。从这个事情我们以后调试得注意一定得注意函数调用逻辑链条确保这个函数被执行是从你的认为的那个上级函数调用的。要设置两个断点或者用F11从上级函数步进的方式。13.传参问题在吃午饭时我都要考虑过暂时忽略这个问题不使用事务了因为这已经耽误我一天的时间了。然后我浏览了下代码发现有这样一个函数。if (fCreate !Exists(string(version))) WriteVersion(VERSION);这个是写version字符串的还刻我们之前的传参问题吗也是字符串变成了version这两者有什么关联呢但是这是个WriteVersion函数跟Write完全无关啊。也不是重载啊。然后它也调用write函数写数据bool WriteVersion(int nVersion) { return Write(string(version), nVersion); }但此时我认为我也没执行这个函数啊然后我看了一下version定义#define VERSION 70001 // 示例版本号可根据需要调整竟然是70001,键值都对上了得传参问题跟这个函数有关。结合上面的GetTxn乌龙我恍然大悟看下面语句这是CDB构造函数据语句if (fCreate !Exists(string(version))) WriteVersion(VERSION);因为我文件模式是cr所以fCreaete总为真又因为put执行不成功所以数据库次次都没有这个version信息导致程序导致每次都会执行一下WirteVersion而因为我设置的Write是单断点。所以调试的是WriteVersion对write的调用导致我误认为参数不对的。但是我记得我是前后设置了两个断点啊可能两个断点隔的太远了把WriteVersion也包含进去了。问题已经知道了我严格两个断点一致和F11函数步进都试了一遍这回发现传进去的参数正确。之前是经验主义想当然了吧。好这个传参问题已经解决最终的问题只有一个那就是事务。我再看看代码吧如果实在解决不了就临时不用事务写了。14.最终真相等等这里面好像有个逻辑错误。1.首先我当时调试的时候发现write函数传进来的参数是version,70001,然后调用put报错。那么是不是调用WriteVersion也会引发错误。2.然后我现在是bool CTxDB::WriteBlockIndex(const CDiskBlockIndex blockindex)测试put的啊它是怎么跨过第一次错误的呢跑到我这个语句里来的呢在这个思考的过程中我已经找到了答案就是报错的原因终于找到了应该靠谱。这个先不验证了。但是第一次那个现象我还没有理清等我还原一下。先把文件删了从第一步开始。好首先CDB构造函数里的WriteVersion,最终会执行Write里的这句int ret pdb-put(GetTxn(), datKey, datValue, (fOverwrite ? 0 : DB_NOOVERWRITE));此时GetTxn返回的还是null不使用事务所以不会报错那么就写入了version而不是我之前的推断。写入之后就不会再调用WriteVersion了而我调试的时候设置断点只看了参数就又退出了偶尔又不调试让程序直接报错那么偶然的这一次就写入了version这就导致了上面的第2个逻辑发生。所以我将文件都删除了重新测试了一下发现确实是这样。好了但是这样一分析又打破了我之前说的找到问题的逻辑。我原以为是WriteVerion调用write把我的事务给消耗了跟我put有冲突的导致我后面写区块索引时用的是废弃的事务但是按照后来的逻辑WriteVersion使用的是空事务。而且写好后下次就不会执行了。应该不会跟我的put有冲突。但是弯打正着吧我为此想到一个解决方法但确实解决了由此我找到了真正的原因。15.DB_AUTO_COMMIT最终问题在这里CTxDB txdb(cr); txdb.TxnBegin();我不是为了创建文件(不调用LoadBlockIndex)构造txdb时加了cr模式吗不使用默认的r模式。然后导致这里CDB::CDB(const char* pszFile, const char* pszMode, bool fTxn) : pdb(NULL) { int ret; if (pszFile NULL) return; bool fCreate strchr(pszMode, c); bool fReadOnly (!strchr(pszMode, ) !strchr(pszMode, w)); unsigned int nFlags DB_THREAD; if (fCreate) nFlags | DB_CREATE; else if (fReadOnly) nFlags | DB_RDONLY; if (!fReadOnly || fTxn) nFlags | DB_AUTO_COMMIT;导致fReadOnly为真然后最后一个判断!fReadOnly就为假而fTxn这个事务指示参数为假。所以nFlags|DB_AUTO_COMMIT这句不会被执行也就是说数据库打开没有加上DB_AUTO_COMMIT标志导致的报错。原因就是这个现在创建了文件后你去掉cr则不报错了。比如这样CTxDB txdb; txdb.TxnBegin();又比如就算有cr,你只要强行加上nFlags | DB_AUTO_COMMIT;也不会出错问题就是出在这里源码中也是没有cr的。但是DB_AUTO_COMMIT;的意思是自动事务也就是你调用put写数据它会在内部自动创建一个事务然后自动提交。只针对单条。那源码中为什么后面的代码还用手动管理呢而且因为我是手动管理的不添加DB_AUTO_COMMIT应该也是可以的。这些需要最小化系统用实际代码去测试总结这里就不去研究了因为后面btc使用的是LevelDB数据库DB已经过时了不必太花费心思去研究就算是一样的架构以后也会用LevelDB重构CDB类吧我们这里跟源码一致不写cr就行知道情况就行。好我们删除创建的数据文件重新来过。16.LoadBlockIndex然后在main.cpp写一个全局函数LoadBlockIndex这里我们就不复制源码中的代码否则修复代码说明又会占很大篇幅这里只创建一下文件bool LoadBlockIndex() { CTxDB txdb(cr); txdb.Close(); return true; }然后在mian.h中声明一下。AddToBlockIndex函数里的这部分代码改一下也就是去掉cr:CTxDB txdb; txdb.TxnBegin(); txdb.WriteBlockIndex(CDiskBlockIndex(pindexNew));17.再次测试然后main里测试代码int main() { CBlock b GenerateTestBlock(); LoadBlockIndex(); bool resb.AcceptBlock(); if (res) { cout 写区块数据和写索引目录成功! endl; auto it mapBlockIndex.begin(); // 迭代器指向第一个元素 uint256 firstHash it-first; cout 索引哈希值: firstHash.ToString() endl; CBlockIndex* pIndex it-second; cout 索引区块中的时间:(对比是否一致) pIndex-nTimeendl; } return 0; }测试正常记住时间这个值跟GenerateTestBlock填的一致下一章中我们将会从数据库里读取出来再来对比一下这个值验证是否正确。

更多文章