创建一个阻塞队列以容纳任务,在第一次执行任务时创建做够多的线程(不超过许可线程数),并处理任务,之后每个工作线程自行从任务对列中获得任务,直到任务队列中的任务数量为0为止,此时,线程将处于等待状态,一旦有任务再加入到队列中,即召唤醒工作线程进行处理,实现线程的可复用性。
使用线程池减少的是线程的创建和销毁时间,这对于多线程应用来说非常有帮助,比如我们常用的Servlet容器,每次请求处理的都是一个线程,如果不采用线程池技术,每次请求都会重新创建一个新的线程,这会导致系统的性能符合加大,响应效率下降,降低了系统的友好性。
本小节主要介绍 txdb 以及其所引用到的代码中一些常量所表示的含义
在 txdb.cpp
中,我们能够看到其定义了很多 char 类型的常量:
static const char DB_COIN = 'C';
static const char DB_COINS = 'c';
static const char DB_BLOCK_FILES = 'f';
static const char DB_TXINDEX = 't';
static const char DB_BLOCK_INDEX = 'b';
static const char DB_BEST_BLOCK = 'B';
static const char DB_FLAG = 'F';
static const char DB_REINDEX_FLAG = 'R';
static const char DB_LAST_BLOCK = 'l';
它们所代表的具体意思如下所示:
Block 模块(Key-value pairs)
在LevelDB中,使用的键/值对解释如下:
'b' + 32 字节的 block hash -> 记录块索引,每个记录存储:
* 块头(block header)
* 高度(height)
* 交易的数量
* 这个块在多大程度上被验证
* 块数据被存储在哪个文件中
* undo data 被存储在哪个文件中。
'f' + 4 字节的文件编号 -> 记录文件信息。每个记录存储:
* 存储在具有该编号的块文件中的块的数量
* 具有该编号的块文件的大小($ DATADIR / blocks / blkNNNNN.dat)
* 具有该编号的撤销文件的大小($ DATADIR / blocks / revNNNNN.dat)。
* 使用该编号存储在块文件中的块的最低和最高高度。
* 使用该编号存储在块文件中的块的最小和最大时间戳。
'l' - > 4个字节的文件号:使用的最后一个块文件号。
'R' - > 1字节布尔值(如果为“1”):是否处于重新索引过程中。
'F'+ 1个字节的标志名长度+标志名字符串 - > 1个字节布尔型('1'为真,'0'为假):可以打开或关闭的各种标志。 目前定义的标志包括:
* 'txindex':是否启用事务索引。
't'+ 32字节的交易 hash - >记录交易索引。 这些是可选的,只有当'txindex'被启用时才存在。 每个记录存储:
* 交易存储在哪个块文件号码中。
* 哪个文件中的交易所属的块被抵消存储在。
* 从该块的开始到该交易本身被存储的位置的偏移量。
utxo 模块(Key-value pairs)
'c'+ 32字节的交易hash - >记录该交易未使用的交易输出。 这些记录仅对至少有一个未使用输出的事务处理。 每个记录存储:
* 交易的版本。
* 交易是否是一个coinbase或没有。
* 哪个高度块包含交易。
* 该交易的哪些输出未使用。
* scriptPubKey和那些未使用输出的数量。
'B' - > 32字节block hash:数据库表示未使用的交易输出的 block hash。
在 txdb.h
文件中,我们能够看到如下定义,它们所表示的含义如下:
//在flush时,会额外补偿这么多的 memory peak
static constexpr int DB_PEAK_USAGE_FACTOR = 2;
//如果当前可用空间在这个范围之内的话,则无需定期刷新。
static constexpr int MAX_BLOCK_COINSDB_USAGE = 200 * DB_PEAK_USAGE_FACTOR;
//如果少于这个空间仍然可用,会定期刷新
static constexpr int MIN_BLOCK_COINSDB_USAGE = 50 * DB_PEAK_USAGE_FACTOR;
//DB Cache 的默认大小
static const int64_t nDefaultDbCache = 450;
//DB Cache的最大值
static const int64_t nMaxDbCache = sizeof(void *) > 4 ? 16384 : 1024;
//DB Cache 的最小值
static const int64_t nMinDbCache = 4;
//如果没有txIndex的话,内存最大分配给block tree DB的空间。
static const int64_t nMaxBlockDBCache = 2;
//如果有 txIndex 的话,内存最大分配给block tree DB的空间。
//与UTXO数据库不同,对于leveldb缓存创建的txindex方案
static const int64_t nMaxBlockDBAndTxIndexCache = 1024;
//内存最大分配给coins DB的缓存大小
static const int64_t nMaxCoinsDBCache = 8;
在 dbwrapper.h
文件的 class CDBWrapper
下,定义了在操作leveldb
时的一些选项,其具体含义如下所示:
//该数据库使用自定义环境(在默认环境情况下,可以是nullptr)
leveldb::Env *penv;
//数据库使用选项
leveldb::Options options;
//从数据库读取时使用的选项
leveldb::ReadOptions readoptions;
//迭代数据库的值时使用的选项
leveldb::ReadOptions iteroptions;
//写入数据库时使用的选项
leveldb::WriteOptions writeoptions;
//同步写入数据库时使用的选项
leveldb::WriteOptions syncoptions;
//数据库本身
leveldb::DB *pdb;
在 chain.h
的 CBlockFileInfo 下,有如下常量:
class CBlockFileInfo {
public:
//文件中存储的块的数量
unsigned int nBlocks;
//块文件使用的字节数
unsigned int nSize;
//撤消文件需要使用的字节数
unsigned int nUndoSize;
//文件中块的最低高度
unsigned int nHeightFirst;
//文件中块的最高高度
unsigned int nHeightLast;
//文件中最早的块
uint64_t nTimeFirst;
//文件中最新的块的时间
uint64_t nTimeLast;
在 chain.h
的 BlockStatus 文件下,列举了一些状态,用来标识 block的状态:
enum BlockStatus : uint32_t {
//未使用
BLOCK_VALID_UNKNOWN = 0,
//解析正确、版本正确并且 hash 满足声明 PoW,1 <= vtx count <= max,时间戳正确。
BLOCK_VALID_HEADER = 1,
//找到所有父标题,难度匹配,时间戳> =中位数前一个检查点。意味着所有的父母至少也是TREE。
BLOCK_VALID_TREE = 2,
// 只有第一个 tx 是 coinbase,2 <= coinbase输入脚本长度<= 100,
//交易有效,没有重复的txids,sigops,大小,merkle根。
//所有父母至少是TREE,但不一定是TRANSACTIONS。
//当所有父块都有TRANSACTIONS时,CBlockIndex :: nChainTx将被设置。
BLOCK_VALID_TRANSACTIONS = 3,
//输出不会超支输入,没有双重花费,coinbase输出正常,
//没有不成熟的硬币,BIP30。
//所有的父母也至少包含在链中。
BLOCK_VALID_CHAIN = 4,
//脚本和签名确定。意味着所有的父母也至少是脚本。
BLOCK_VALID_SCRIPTS = 5,
// 所有的有效位。
BLOCK_VALID_MASK = BLOCK_VALID_HEADER | BLOCK_VALID_TREE |
BLOCK_VALID_TRANSACTIONS | BLOCK_VALID_CHAIN |
BLOCK_VALID_SCRIPTS,
//blk * .dat中的完整块
BLOCK_HAVE_DATA = 8,
//撤销rev * .dat中的可用数据
BLOCK_HAVE_UNDO = 16,
BLOCK_HAVE_MASK = BLOCK_HAVE_DATA | BLOCK_HAVE_UNDO,
//上次达到有效性的阶段失败
BLOCK_FAILED_VALID = 32,
//从失败块下降
BLOCK_FAILED_CHILD = 64,
BLOCK_FAILED_MASK = BLOCK_FAILED_VALID | BLOCK_FAILED_CHILD,
};
首先 txdb
模块主要是用来实现 block
和 utxo
两个模块的落盘逻辑,所以我们将分为两个大的部分,来对其逻辑一一梳理。
原始数据块
首先,我们通过网络接收到原始块,进行块文件存储。
访问块数据文件
块文件通过以下方式访问:
- CDiskTxPos:一个 struct,
CDiskTxPos
继承CDiskBlockPos
,CDiskBlockPos
主要有两个参数nFile
和nPos
, 指向一个块在磁盘上的位置的指针(一个文件号和偏移量):
struct CDiskTxPos : public CDiskBlockPos {
unsigned int nTxOffset; // after header
ADD_SERIALIZE_METHODS;
template <typename Stream, typename Operation>
inline void SerializationOp(Stream &s, Operation ser_action) {
READWRITE(*(CDiskBlockPos *)this);
READWRITE(VARINT(nTxOffset));
}
CDiskTxPos(const CDiskBlockPos &blockIn, unsigned int nTxOffsetIn)
: CDiskBlockPos(blockIn.nFile, blockIn.nPos), nTxOffset(nTxOffsetIn) {}
CDiskTxPos() { SetNull(); }
void SetNull() {
CDiskBlockPos::SetNull();
nTxOffset = 0;
}
};
- CBlockFileInfo :该函数用于执行如下任务:
- 确定新块是否适合当前文件或需要创建新文件
- 按块和撤消文件计算总的磁盘使用率
- 遍历块文件并找到可修剪的文件
数据库条目跟踪每个块文件已经有多少个字节使用,它有多少块,高度的范围是存在的以及日期的范围。
Block index
块索引保存所有已知块的元数据,包括块在磁盘上的存储位置。
区块链是一个树状结构,从根部的生成区块开始,每个区块可能有多个候选区块作为下一个区块。 blockindex 可能有多个 pprev 指向它,但是它们中至多有一个可以是当前活动分支的一部分。
实际上,LevelDB 的块索引是通过 txdb.h 中定义的 CBlockTreeDB
包装类来访问的。 请注意,不同的节点会有略微不同的块树, 重要的是看他们是否同意主链。
存储在数据库中的块在内存中表示为 CBlockIndex
对象。 这种类型的对象首先在收到 header 后创建; 代码不会等待收到完整的块。 当通过网络接收到 header时,它们被传输到一个 CBlockHeaders
矢量中,然后被检查。 检出的每个header 都会导致创建一个新的CBlockIndex
,并将其存储到数据库中。
block index有两个重要的变量
- nTx:这个块的交易数量。nTx > 0 表示该块的状态至少为
VALID_TRANSACTIONS。 - nChainTx:包括此块在内的链中的交易数量,当且仅当此块及其所有父项的交易可用时,才会设置此值。
因此,nChainTx> 0
是一个 VALID_TRANSACTIONS 链的简写。 注意,这个信息不能通过块状态枚举来获得。 也就是说,VALID_TRANSACTIONS 只意味着它的父母是 TREE,而 VALID_CHAIN 意味着父母也是 CHAIN。 因此,从某种意义上来说,表达式(nChainTx!= 0)是可以被称为 “VALID_nChainTx = 3.5”的状态的缩写 - 因为它比VALID_TRANSACTIONS更多但是小于VALID_CHAIN。
注意:nChainTx只存储在内存中; 数据库中没有对应的条目。
class CBlockIndex {
public:
void SetNull() {
...
}
CBlockIndex() { SetNull(); }
CBlockIndex(const CBlockHeader &block) {
...
}
CDiskBlockPos GetBlockPos() const {
...
}
CDiskBlockPos GetUndoPos() const {
...
}
CBlockHeader GetBlockHeader() const {
...
}
uint256 GetBlockHash() const { return *phashBlock; }
int64_t GetBlockTime() const { return (int64_t)nTime; }
int64_t GetBlockTimeMax() const { return (int64_t)nTimeMax; }
enum { nMedianTimeSpan = 11 };
int64_t GetMedianTimePast() const {
...
}
std::string ToString() const {
...
}
//! Check whether this block index entry is valid up to the passed validity
//! level.
bool IsValid(enum BlockStatus nUpTo = BLOCK_VALID_TRANSACTIONS) const {
...
}
//! Raise the validity level of this block index entry.
//! Returns true if the validity was changed.
bool RaiseValidity(enum BlockStatus nUpTo) {
...
}
//! Build the skiplist pointer for this entry.
void BuildSkip();
//! Efficiently find an ancestor of this block.
CBlockIndex *GetAncestor(int height);
const CBlockIndex *GetAncestor(int height) const;
};
在启动时,LoadBlockIndexGuts
将整个数据库加载到内存中,这只需要几秒钟。
bool CBlockTreeDB::LoadBlockIndexGuts(
std::function<CBlockIndex *(const uint256 &)> insertBlockIndex) {
std::unique_ptr<CDBIterator> pcursor(NewIterator());
pcursor->Seek(std::make_pair(DB_BLOCK_INDEX, uint256()));
// Load mapBlockIndex
while (pcursor->Valid()) {
...
}
return true;
}
mapBlockIndex (map<block_hash, CBlockIndex*>)
mapBlockIndex 包含所有已知的块(“块”-->“块索引”)。上面我们提到,由于在收到 header 时就创建了块索引并将其存储在 LevelDB 中,因此在块映射中可能没有收到完整块的块索引,更不用说将其存储到磁盘了。
mapBlockIndex 是没有排序的。只要把它想象成你的块块哈希( LevelDB)在内存中。
mapBlockIndex 是从 LoadBlockIndexGuts 中的数据库初始化的,LoadBlockIndexGuts 在启动的时运行。此后,无论何时通过网络接收到新块,都会更新。
mapBlockIndex 只会增长,它永远不会缩小。 (还要注意,块索引的 LevelDB 包装器不包含从数据库中删除块的功能 - 它的写入函数(WriteBatchSync
)只写入数据库。相比之下,chainstate 包装器的写入功能(BatchWrite
)可以写入和删除。
块( 'b' 键)被加载到全局 mapBlockIndex 变量中。 mapBlockIndex 是一个unordered_map,它为整个块树中的每个块保存 CBlockIndex。
bool CBlockTreeDB::WriteBatchSync(
const std::vector<std::pair<int, const CBlockFileInfo *>> &fileInfo,
int nLastFile, const std::vector<const CBlockIndex *> &blockinfo) {
CDBBatch batch(*this);
for (std::vector<std::pair<int, const CBlockFileInfo *>>::const_iterator
it = fileInfo.begin();
it != fileInfo.end(); it++) {
batch.Write(std::make_pair(DB_BLOCK_FILES, it->first), *it->second);
}
batch.Write(DB_LAST_BLOCK, nLastFile);
for (std::vector<const CBlockIndex *>::const_iterator it =
blockinfo.begin();
it != blockinfo.end(); it++) {
batch.Write(std::make_pair(DB_BLOCK_INDEX, (*it)->GetBlockHash()),
CDiskBlockIndex(*it));
}
return WriteBatch(batch, true);
}
block 状态
其中一个关键特征就是它的 “验证状态” 。
验证状态不仅会验证当前块,还会去验证其祖先块。
该块的状态是下面的其中一种:
enum BlockStatus : uint32_t {
//未使用
BLOCK_VALID_UNKNOWN = 0,
//解析时版本正常,哈希声明满足PoW,1 <= vtx count <= max,时间戳不在将来
BLOCK_VALID_HEADER = 1,
//找到所有父标题,难度匹配,时间戳> =中位数前一个检查点。 意味着所有的父母至少也是TREE。
BLOCK_VALID_TREE = 2,
//只有第一个tx是coinbase,2 <= coinbase输入脚本长度<= 100,
//交易有效,没有重复的txids,sigops,大小,merkle根。
//意味着所有父母至少是TREE,但不一定是TRANSACTIONS。
//当所有父块都有TRANSACTIONS时,CBlockIndex :: nChainTx将会被设置。
BLOCK_VALID_TRANSACTIONS = 3,
// 输出不会超支输入,没有双重花费,coinbase输出正常,
// 没有不成熟的硬币,BIP30。
// 意味着所有的父母也至少在链中。
BLOCK_VALID_CHAIN = 4,
// 脚本和签名确定。 意味着所有的父母也至少是脚本。
BLOCK_VALID_SCRIPTS = 5,
};
CDBWrapper
CDBWrapper是一个leveldb的包装函数,无论utxo还是block,均通过它写入leveldb,具体参照下图:
dbWrapper
CDBWrapper
主要有如下参数:
- path -->系统中存储leveldb数据的位置
- nCacheSize -->配置各种leveldb缓存设置
- fMemory --> 如果为true,则使用leveldb的内存环境
- fWipe --> 如果为true,则删除所有现有数据
- obfuscate --> 如果为true,则通过简单的XOR存储数据。 如果为false,则与零字节数组进行异或运算
class CDBWrapper {
public:
CDBWrapper(const boost::filesystem::path &path, size_t nCacheSize,
bool fMemory = false, bool fWipe = false,
bool obfuscate = false);
~CDBWrapper();
};
UTXO
访问 UTXO 数据库比块索引复杂得多。 这是因为它的性能对比特币系统的整体性能至关重要。 块索引对于性能来说并不是很关键,因为只有几十万个块,在好的硬件上运行的节点可以在几秒钟内检索并滚动(而且不需要经常这样做)。在UTXO数据库中有数百万个coins,并且必须对每个进入mempool或包含在块中的每个输入的输入进行检查和修改。
在 init.cpp
文件的 1941-1946,我们会发现,utxo数据库在这里被初始化。
pblocktree = new CBlockTreeDB(nBlockTreeDBCache, false, fReindex);
pcoinsdbview = new CCoinsViewDB(nCoinDBCache, false, fReindex || fReindexChainState);
pcoinscatcher = new CCoinsViewErrorCatcher(pcoinsdbview);
pcoinsTip = new CCoinsViewCache(pcoinscatcher);
上述代码首先初始化一个CoinsViewDB
,它有从LevelDB中加载 coin 的方法。
接下来,初始化pCoinsTip,它是代表活动链状态的高速缓存,并由数据库视图支持。
pCoinsTip
保存对应于活动链的提示的 UTXO 集合, 检索/刷新到数据库视图。
coins.cpp
中的 FetchCoins
函数演示了代码如何使用缓存与数据库:
1 CCoinsMap::iterator it = cacheCoins.find(outpoint);
2 if (it != cacheCoins.end()) {
3 return it; }
4 Coin tmp;
5 if (!base->GetCoin(outpoint, tmp)) {
6 return cacheCoins.end(); }
7 CCoinsMap::iterator ret = cacheCoins.emplace(std::piecewise_construct, std::forward_as_tuple(outpoint), std::forward_as_tuple(std::move(tmp))).first;
- 首先,代码在缓存中搜索给定交易ID的硬币 (第1行)
- 如果找到,它返回“提取”的硬币 (2-3行)
- 如果不是,则搜索数据库 (第5行)
- 如果在数据库中找到,它会更新缓存(第7行)
CCoinsViewDBCursor
CCoinsViewDBCursor
继承自CCoinsViewCursor
,专门用来迭代CCoinsViewDB
:
class CCoinsViewDBCursor : public CCoinsViewCursor {
public:
~CCoinsViewDBCursor() {}
bool GetKey(COutPoint &key) const;
bool GetValue(Coin &coin) const;
unsigned int GetValueSize() const;
bool Valid() const;
void Next();
private:
CCoinsViewDBCursor(CDBIterator *pcursorIn, const uint256 &hashBlockIn)
: CCoinsViewCursor(hashBlockIn), pcursor(pcursorIn) {}
std::unique_ptr<CDBIterator> pcursor;
std::pair<char, COutPoint> keyTmp;
friend class CCoinsViewDB;
};
CCoinsViewDB
CCoinsViewDB
继承自 CCoinsView
,CCoinsView 由 coin 数据库备份(chainstate /),主要与 leveldb 进行交互。它会根据 chainstate
在 LevelDB 设置的 UTXO, 检索 coins 并且 flush 到 LevelDB 的变化:
class CCoinsViewDB : public CCoinsView {
protected:
CDBWrapper db;
public:
CCoinsViewDB(size_t nCacheSize, bool fMemory = false, bool fWipe = false);
bool GetCoin(const COutPoint &outpoint, Coin &coin) const override;
bool HaveCoin(const COutPoint &outpoint) const override;
uint256 GetBestBlock() const override;
bool BatchWrite(CCoinsMap &mapCoins, const uint256 &hashBlock) override;
CCoinsViewCursor *Cursor() const override;
//! Attempt to update from an older database format.
//! Returns whether an error occurred.
bool Upgrade();
size_t EstimateSize() const override;
};
CoinEntry
CoinEntry
是一个基础结构,服务于CCoinsViewDB
:
struct CoinEntry {
COutPoint *outpoint;
char key;
CoinEntry(const COutPoint *ptr)
: outpoint(const_cast<COutPoint *>(ptr)), key(DB_COIN) {}
template <typename Stream> void Serialize(Stream &s) const {
s << key;
s << outpoint->hash;
s << VARINT(outpoint->n);
}
template <typename Stream> void Unserialize(Stream &s) {
s >> key;
s >> outpoint->hash;
s >> VARINT(outpoint->n);
}
};
引用
一篇文章主要介绍了,txdb 的一个整体逻辑,本文将详细描述 txdb 模块与 leveldb 的交互,以及对 leveldb 的封装。
上一篇文章提到,在 dbwrapper.h
的 CDBWrapper
是对 leveldb 的一个简单封装,所有要写入 leveldb 的东西都会调用 CDBWrapper
这个类,下面我们就来分析一下如何调用,以及 CDBWrapper
究竟实现了哪些逻辑。
CDBWrapper 的构造函数
class CDBWrapper {
public:
CDBWrapper(const boost::filesystem::path &path, size_t nCacheSize,
bool fMemory = false, bool fWipe = false,
bool obfuscate = false);
~CDBWrapper();
};
CDBWrapper
主要有如下参数:
- path -->系统中存储leveldb数据的位置
- nCacheSize -->配置各种leveldb缓存设置
- fMemory --> 如果为true,则使用leveldb的内存环境
- fWipe --> 如果为true,则删除所有现有数据
- obfuscate --> 如果为true,则通过简单的XOR存储数据; 如果为false,则与零字节数组进行异或运算
Read 函数
CDataStream 我们简单理解为一个内存 buffer,read()传入的key是经过泛化的一个K,经过 serialize 之后,相当于一个内存的 slice,ssKey << key;
将这个内存 slice 写入内存 buffer-->sskey 中,之后使用 leveldb 的 Slice 方法写入,ssKey.data()
代表首地址 ,ssKey.size()
表示其大小。
之后我们构造一个 string 的 strValue
,调用 leveldb的 Get(),传入readoptions
读选项,slKey
和构造好的 string 的 strValue
。
最后,我们通过 ssValue
去 leveldb 中拿到我们所要的 value,并经过 Xor(异或运算)进行解码之后,ssValue >> value;
把值塞回给 read 的 value 参数,这样,我们就通过确定的 key 拿到其对应的 value。
在上述过程中,我们发现,我们从leveldb中拿到的值是经过 Xor 编码之后写入的,我们最后读取出来需要经过 Xor 解码的过程,那么编码的过程在哪里呢?很明显,读的逆向操作是 write 操作。
template <typename K, typename V> bool Read(const K &key, V &value) const {
CDataStream ssKey(SER_DISK, CLIENT_VERSION);
ssKey.reserve(DBWRAPPER_PREALLOC_KEY_SIZE);
ssKey << key;
leveldb::Slice slKey(ssKey.data(), ssKey.size());
std::string strValue;
leveldb::Status status = pdb->Get(readoptions, slKey, &strValue);
if (!status.ok()) {
if (status.IsNotFound()) return false;
LogPrintf("LevelDB read failure: %s\n", status.ToString());
dbwrapper_private::HandleError(status);
}
try {
CDataStream ssValue(strValue.data(),
strValue.data() + strValue.size(), SER_DISK,
CLIENT_VERSION);
ssValue.Xor(obfuscate_key);
ssValue >> value;
} catch (const std::exception &) {
return false;
}
return true;
}
Write 函数
在这个函数中,我们能清楚的理解到,write函数首先调用CDBBatch
的write函数,最后返回WriteBatch
批量写函数,所以,我们写入leveldb的过程是一个批量写的过程。
template <typename K, typename V>
bool Write(const K &key, const V &value, bool fSync = false) {
CDBBatch batch(*this);
batch.Write(key, value);
return WriteBatch(batch, fSync);
}
CDBBatch 的 write函数
write 函数的参数同样是泛化过的 K,V,我们通过 ssKey << key;
和 ssValue << value;
将key 和 value 塞进内存 buffer 中,最后通过 Xor 编码之后,调用 put 函数写入 leveldb 中,leveldb::Slice slKey(ssKey.data(), ssKey.size());
依旧表示的是首地址以及大小,slValue
同理。
class CDBBatch {
friend class CDBWrapper;
private:
CDataStream ssKey;
CDataStream ssValue;
public:
template <typename K, typename V> void Write(const K &key, const V &value) {
ssKey.reserve(DBWRAPPER_PREALLOC_KEY_SIZE);
ssKey << key;
leveldb::Slice slKey(ssKey.data(), ssKey.size());
ssValue.reserve(DBWRAPPER_PREALLOC_VALUE_SIZE);
ssValue << value;
ssValue.Xor(dbwrapper_private::GetObfuscateKey(parent));
leveldb::Slice slValue(ssValue.data(), ssValue.size());
batch.Put(slKey, slValue);
// LevelDB serializes writes as:
// - byte: header
// - varint: key length (1 byte up to 127B, 2 bytes up to 16383B, ...)
// - byte[]: key
// - varint: value length
// - byte[]: value
// The formula below assumes the key and value are both less than 16k.
size_estimate += 3 + (slKey.size() > 127) + slKey.size() +
(slValue.size() > 127) + slValue.size();
ssKey.clear();
ssValue.clear();
}
WriteBatch函数
WriteBatch的参数fSync 判断write的过程是否为同步write。
bool WriteBatch(CDBBatch &batch, bool fSync = false);
Exists 函数
Exists 函数的参数是一个泛化后的 key,通过 key 可以判断该 key 所对应的 value 究竟是否在 leveldb 中存在。
template <typename K> bool Exists(const K &key) const {
CDataStream ssKey(SER_DISK, CLIENT_VERSION);
ssKey.reserve(DBWRAPPER_PREALLOC_KEY_SIZE);
ssKey << key;
leveldb::Slice slKey(ssKey.data(), ssKey.size());
std::string strValue;
leveldb::Status status = pdb->Get(readoptions, slKey, &strValue);
if (!status.ok()) {
if (status.IsNotFound()) return false;
LogPrintf("LevelDB read failure: %s\n", status.ToString());
dbwrapper_private::HandleError(status);
}
return true;
}
Erase 函数
Erase
函数与write
函数同理,首先调用CDBBatch
中的Erase
函数,最后返回WriteBatch
。不同的是 Erase 函数用来删除传入的 key 所定位到的 value。
template <typename K> bool Erase(const K &key, bool fSync = false) {
CDBBatch batch(*this);
batch.Erase(key);
return WriteBatch(batch, fSync);
}
CDBBatch 的 Erase
函数
思路同理,同样是泛化后的key,写入内存buffer --->ssKey中,然后通过leveldb::Slice
判断这个key的首地址和大小,调用batch.Delete(slKey);
将其删除。ssKey.clear();
代表删除内存中的临时变量。
template <typename K> void Erase(const K &key) {
ssKey.reserve(DBWRAPPER_PREALLOC_KEY_SIZE);
ssKey << key;
leveldb::Slice slKey(ssKey.data(), ssKey.size());
batch.Delete(slKey);
// LevelDB serializes erases as:
// - byte: header
// - varint: key length
// - byte[]: key
// The formula below assumes the key is less than 16kB.
size_estimate += 2 + (slKey.size() > 127) + slKey.size();
ssKey.clear();
}
Flush函数
Flush
函数需要注意,它并不适用于LevelDB,不是我们想象中的将要写入的数据flush到leveldb中, 只是提供与BDB的兼容性:
bool Flush() {
return true;
}
Sync 函数
sync
函数用来判断批量写入的时候是否采用同步的方式:
bool Sync() {
CDBBatch batch(*this);
return WriteBatch(batch, true);
}
NewIterator 函数
该函数返回的返回值是CDBIterator
:
CDBIterator *NewIterator() {
return new CDBIterator(*this, pdb->NewIterator(iteroptions));
}
下面我们来分析一下CDBIterator
:
CDBIterator
CDBIterator 包含两个参数,_parent
表示父CDBWrapper
的实例;_piter
表示原始的leveldb迭代器。
class CDBIterator {
private:
const CDBWrapper &parent;
leveldb::Iterator *piter;
public:
CDBIterator(const CDBWrapper &_parent, leveldb::Iterator *_piter)
: parent(_parent), piter(_piter){};
~CDBIterator();
}
换句话说, CDBIterator
就是对leveldb::Iterator
的封装,并且封装了如下函数来供使用
Seek
Seek 函数,通过泛化的 key 写入 ssKey,之后获取首地址以及大小,传入leveldb内部的seek函数来实现查找的功能。
template <typename K> void Seek(const K &key) {
CDataStream ssKey(SER_DISK, CLIENT_VERSION);
ssKey.reserve(DBWRAPPER_PREALLOC_KEY_SIZE);
ssKey << key;
leveldb::Slice slKey(ssKey.data(), ssKey.size());
piter->Seek(slKey);
}
其他函数同理,主要实现有:
bool Valid(); //确认是否有效
void SeekToFirst(); //从头开始找
void Next(); //获取下一个元素
bool GetKey(K &key) //获取key
int GetKeySize() //获取key的size
bool GetValue(V &value) //获取value
int GetValueSize() //后去value的size
IsEmpty函数
IsEmpty函数返回一个bool类型,他的作用和她的字面意思是一样的,如果IsEmpty 管理的数据库不包含数据,则返回true。
bool IsEmpty();
EstimateSize函数
EstimateSize 函数用来预估从 key_begin 到 key_end 的范围占了文件系统多大的空间,pdb->GetApproximateSizes(&range, 1, &size);
这个函数是 leveldb 内部的一个函数具体含义如下:
GetApproximateSizes
方法是用来获取一个或多个密钥范围内使用的文件系统空间的近似大小。
leveldb::Range ranges[2];
ranges[0] = leveldb::Range("a", "c");
ranges[1] = leveldb::Range("x", "z");
uint64_t sizes[2];
leveldb::Status s = db->GetApproximateSizes(ranges, 2, sizes);
GetApproximateSizes(ranges, 2, sizes) 的第二个参数 2 就是 uint64_t sizes[2]
中的 2 ,因为在 cpp 中数组作为参数时会退化,所以第三个参数 sizes
只是一个指针,2 代表的是获取上面两个range所占的大小。
size_t EstimateSize(const K &key_begin, const K &key_end) const {
CDataStream ssKey1(SER_DISK, CLIENT_VERSION),
ssKey2(SER_DISK, CLIENT_VERSION);
ssKey1.reserve(DBWRAPPER_PREALLOC_KEY_SIZE);
ssKey2.reserve(DBWRAPPER_PREALLOC_KEY_SIZE);
ssKey1 << key_begin;
ssKey2 << key_end;
leveldb::Slice slKey1(ssKey1.data(), ssKey1.size());
leveldb::Slice slKey2(ssKey2.data(), ssKey2.size());
uint64_t size = 0;
leveldb::Range range(slKey1, slKey2);
pdb->GetApproximateSizes(&range, 1, &size);
return size;
}
leveldb库提供了一个持久性的键值存储。 键和值是任意字节数组。 keys 根据用户指定的比较器功能在 key-value store 内排序。
Opening A Database(创建并打开数据库)
leveldb 数据库具有与文件系统目录相对应的名称。 所有数据库的内容都存储在这个目录下。 如有必要创建数据库,下面的例子演示如何打开数据库:
#include <cassert>
#include "leveldb/db.h"
leveldb::DB* db;
leveldb::Options options;
options.create_if_missing = true;
leveldb::Status status = leveldb::DB::Open(options, "/tmp/testdb", &db);
assert(status.ok());
...
如果你想在数据库已经存在的情况下抛出错误的话,可以在leveldb :: DB :: Open
调用之前的行添加以下内容:
options.error_if_exists = true;
Status(状态)
你可能已经注意到上面的leveldb :: Status
类型。 leveldb的函数大都会返回这种类型的值,但是可能会遇到错误。 你可以检查结果是否是正确的,如果错误,打印相关的错误消息:
leveldb::Status s = ...;
if (!s.ok()) cerr << s.ToString() << endl;
Closing A Database(关闭数据库)
当数据库的所有操作都执行完成之后,只需删除数据库对象。 Example:
... open the db as described above ...
... do something with db ...
delete db;
Reads And Writes(读写操作)
数据库提供了 Put,Delete 和 Get 方法来修改/查询数据库。例如,下面的代码将存储在key1下的值移动到key2。
std::string value;
leveldb::Status s = db->Get(leveldb::ReadOptions(), key1, &value);
if (s.ok()) s = db->Put(leveldb::WriteOptions(), key2, value);
if (s.ok()) s = db->Delete(leveldb::WriteOptions(), key1);
Atomic Updates(原子更新)
请注意,如果进程在 Put key2 之后但在 delete key1 之前死亡,多个键下可能会保存相同的值。 这样的问题可以通过使用 WriteBatch 类来避免:
#include "leveldb/write_batch.h"
...
std::string value;
leveldb::Status s = db->Get(leveldb::ReadOptions(), key1, &value);
if (s.ok()) {
leveldb::WriteBatch batch;
batch.Delete(key1);
batch.Put(key2, value);
s = db->Write(leveldb::WriteOptions(), &batch);
}
WriteBatch 可以将数据批量写入数据库,并且能够保证这些批量的批次可以按照顺序使用。 请注意,就像我们上述使用delete的方式,如果key1与key2相同,我们不会错误地将该值丢弃。
除了它原子性的好处外,WriteBatch
也可以将许多单一的修改放入同一批次来批量更新。
Synchronous Writes(同步写)
默认情况下,每次写入leveldb都是异步的:当进程写入操作系统后就会返回。 从操作系统的内存到底层磁盘的传输是异步进行的。对于特定的写操作,可以打开同步sync标志使写操作一直到数据被传输到底层存储器后再返回。 (在Posix系统上,这是通过在写操作返回之前调用fsync(...)
或fdatasync(...)
或msync(...,MS_SYNC)
来实现的。)
leveldb::WriteOptions write_options;
write_options.sync = true;
db->Put(write_options, ...);
异步写入速度通常是同步写入速度的千倍以上。 异步写入的缺点是当机器宕机时,可能导致最后几次更新丢失。 如果是在写入过程中的宕机(而非重新启动),即使sync设置为false,更新操作也会认为已经将更新从内存中推送到了操作系统。
通常可以安全地使用异步写入。比如,当加载大量数据到数据库中时,可以通过在宕机后重新启动批量加载来处理丢失的更新。有一个可用的混合方案,将多次写入的第N次写入设置为同步的,并在宕机重启后的情况下,批量加载由前一次运行的最后一次同步写入之后重新开始。(同步写入时可以更新描述宕机后批量加载重新开始的标记。)
WriteBatch
提供了异步写入的替代方法。 可以将多个更新放置在同一个WriteBatch中,并和同步写入(即write_options.sync设置为true)一起进行。 同步写入的额外开销将在批处理中的所有写入之间进行分摊。
Concurrency(并发)
数据库一次只能由一个进程打开。 leveldb的实现是从操作系统层面获取锁来防止误操作。在一个进程中,相同的leveldb :: DB
对象可以安全地被多个并发线程共享。 即,在同一个数据库中,无需任何外部同步(leveldb会自动执行所需的同步),不同的线程就可以写入、取出 interior 或调用 Get方法。但是其他对象(如迭代器和WriteBatch
)可能需要外部同步。 如果两个线程共享这样的对象,它们必须使用自己的协议锁来保护自己的访问。
Iteration(迭代器)
以下示例演示如何在数据库中打印所有键值对。
leveldb::Iterator* it = db->NewIterator(leveldb::ReadOptions());
for (it->SeekToFirst(); it->Valid(); it->Next()) {
cout << it->key().ToString() << ": " << it->value().ToString() << endl;
}
assert(it->status().ok()); // Check for any errors found during the scan
delete it;
以下变体显示了如何仅处理 range[start,limit)中的键:
for (it->Seek(start); it->Valid() && it->key().ToString() < limit; it->Next()) {
...
}
也可以按相反的顺序处理条目。 (注意:反向迭代可能比正向迭代慢一些。)
for (it->SeekToLast(); it->Valid(); it->Prev()) {
...
}
Snapshots(快照)
snapshot 在 key-value 存储的整个状态中提供一致的只读视图。 ReadOptions :: snapshot
如果是 non-NULL,表示读操作应该在特定版本的DB状态下运行。 如果ReadOptions :: snapshot
为NULL,则读操作将在当前状态的隐式 snapshot 上运行。
Snapshots 由 DB::GetSnapshot()
方法创建:
leveldb::ReadOptions options;
options.snapshot = db->GetSnapshot();
... apply some updates to db ...
leveldb::Iterator* iter = db->NewIterator(options);
... read using iter to view the state when the snapshot was created ...
delete iter;
db->ReleaseSnapshot(options.snapshot);
请注意,当不再需要snapshot时,应该使用DB :: ReleaseSnapshot
接口来释放快照。 这可以减少为维持读取 snapshot 而维护的状态的开销。
Slice(切片)
上面的it-> key()
和it-> value()
调用的返回值是leveldb :: Slice
类型的实例。 Slice是一个简单的结构,它包含一个长度和一个指向外部字节数组的指针。 因为我们不需要复制潜在的大键和值,所以返回一个Slice是返回std :: string
的更便宜的方法。 另外,leveldb方法不会返回以空字符结尾的C风格字符串,因为leveldb键和值允许包含“\ 0”字节。
C ++字符串和以空字符结尾的C风格的字符串可以很容易地转换为Slice:
leveldb::Slice s1 = "hello";
std::string str("world");
leveldb::Slice s2 = str;
切片可以很容易地转换回C ++字符串:
std::string str = s1.ToString();
assert(str == std::string("hello"));
使用切片时要小心,因为调用者要确保在切片使用时切片点保持有效的外部字节数组。 例如,以下是错误示例:
leveldb::Slice slice;
if (...) {
std::string str = ...;
slice = str;
}
Use(slice);
当if语句超出范围时,str 将被销毁,slice 的后备存储将消失。
Comparators(比较器)
前面的例子使用了按照key的默认排序功能,按字典顺序排列字节。 但是,您可以在打开数据库时提供自定义比较器。 例如,假设每个数据库密钥由两个数字组成,我们应该用第一个数字排序,第二个数字打破关系。 首先,定义表达这些规则的“leveldb :: Comparator”的适当子类:
class TwoPartComparator : public leveldb::Comparator {
public:
// Three-way comparison function:
// if a < b: negative result
// if a > b: positive result
// else: zero result
int Compare(const leveldb::Slice& a, const leveldb::Slice& b) const {
int a1, a2, b1, b2;
ParseKey(a, &a1, &a2);
ParseKey(b, &b1, &b2);
if (a1 < b1) return -1;
if (a1 > b1) return +1;
if (a2 < b2) return -1;
if (a2 > b2) return +1;
return 0;
}
// Ignore the following methods for now:
const char* Name() const { return "TwoPartComparator"; }
void FindShortestSeparator(std::string*, const leveldb::Slice&) const {}
void FindShortSuccessor(std::string*) const {}
};
现在用这个自定义比较器创建一个数据库:
TwoPartComparator cmp;
leveldb::DB* db;
leveldb::Options options;
options.create_if_missing = true;
options.comparator = &cmp;
leveldb::Status status = leveldb::DB::Open(options, "/tmp/testdb", &db);
...
Backwards compatibility(向后兼容性)
比较器的 Name 方法的结果会在创建时附加到数据库,并在后续每个打开的数据库上进行检查。 如果名字改变,leveldb :: DB :: Open
调用将失败。 因此,当且仅当新的密钥格式和比较函数与现有的数据库不兼容时才需要更改名称,当然丢弃所有已有数据库的内容是可以的。
然而,你可以通过一些预先规划来逐步演变你的密钥格式。 例如,您可以在每个密钥的末尾存储版本号(一个字节应该足以满足大多数用途)。 当你希望切换到一个新的密钥格式(例如,向由TwoPartComparator处理的密钥添加一个可选的第三部分)时,
(a)保持相同的比较器名称
(b)增加新密钥的版本号
(c)更改比较器功能,以便使用键中的版本号来决定如何解释它们。
Performance(性能)
性能可以通过改变include/leveldb/options.h
中定义的类型的默认值来调整。
Block size(块大小)
leveldb将相邻的键组合在一起成为相同的块,并且这样的块是传送到磁盘和从磁盘传送的单位。 默认的块大小大约是4096个未压缩的字节。 主要对数据库内容进行批量扫描的应用程序可能希望增加此大小。 如果性能测量结果显示有改善,则应用程序执行很多小值的点读取操作可能希望切换到较小的块大小。 使用小于一千字节的块,或者大于几兆字节,没有太大的好处。 另外请注意,压缩将在更大的块大小时更有效。
Compression(压缩)
在写入永久性存储之前,每个块都被单独压缩。 由于默认压缩方法非常快,因此压缩默认为打开状态,并且会自动禁用不可压缩数据。 在极少数情况下,应用程序可能希望完全禁用压缩,但只有在基准测试显示性能得到提高时才应该这样做:
leveldb::Options options;
options.compression = leveldb::kNoCompression;
... leveldb::DB::Open(options, name, ...) ....
Cache(缓存)
数据库的内容存储在文件系统中的一组文件中,每个文件存储一系列压缩块。 如果options.cache不为NULL,则用于缓存经常使用的未压缩块内容。
#include "leveldb/cache.h"
leveldb::Options options;
options.cache = leveldb::NewLRUCache(100 * 1048576); // 100MB cache
leveldb::DB* db;
leveldb::DB::Open(options, name, &db);
... use the db ...
delete db
delete options.cache;
请注意,缓存保存未压缩的数据,因此应根据应用程序级别的数据大小进行调整,而不能从压缩中减少。(压缩块的高速缓存留给操作系统缓冲区高速缓存,或由客户端提供的任何自定义Env实现。)
在执行批量读取时,应用程序可能希望禁用高速缓存,以便批量读取所处理的数据不会取代大部分高速缓存的内容。 每个迭代器选项可以用来实现这一点:
leveldb::ReadOptions options;
options.fill_cache = false;
leveldb::Iterator* it = db->NewIterator(options);
for (it->SeekToFirst(); it->Valid(); it->Next()) {
...
}
Key Layout(键的布局方式)
请注意,磁盘传输和缓存的单位是一个块。 相邻的键(根据数据库排序顺序)通常会放在同一个块中。 因此,应用程序可以通过将彼此靠近的密钥放置在一起,并将不经常使用的密钥放置在密钥空间的单独区域中来改善其性能。
例如,假设我们正在leveldb之上实现一个简单的文件系统。 我们可能希望存储的条目类型是:
filename -> permission-bits, length, list of file_block_ids
file_block_id -> data
我们可能希望用一个字母(如'/')和用不同的字母(比如'0')的file_block_id
作为文件名字母的前缀,这样只扫描元数据就不会强制我们获取和缓存庞大的文件内容。
Filters(过滤器)
由于leveldb数据在磁盘上的组织方式,一个Get()
调用可能涉及从磁盘读取多个数据。 可选的 FilterPolicy 机制可用于大幅减少磁盘读取次数。
leveldb::Options options;
options.filter_policy = NewBloomFilterPolicy(10);
leveldb::DB* db;
leveldb::DB::Open(options, "/tmp/testdb", &db);
... use the database ...
delete db;
delete options.filter_policy;
上面的代码将基于布隆过滤器的过滤策略与数据库相关联。 基于布隆过滤器的过滤依赖于每个键在内存中保留一些数据位(在这种情况下,每个键10位,因为这是我们传递给NewBloomFilterPolicy
的参数)。 这个过滤器可以将Get()调用所需的不必要的磁盘读取次数减少大约100倍。增加每个按键的位数将导致更大的减少,但会增加内存使用量。 我们建议那些工作集不适合内存的应用程序,并且执行大量的随机读操作来设置过滤策略。
如果您使用自定义比较器,则应确保您使用的过滤策略与您的比较器兼容。 例如,考虑一个比较器,比较键时会忽略尾随空格。 NewBloomFilterPolicy
不能与这样的比较器一起使用。 相反,应用程序应该提供一个自定义过滤器策略,也会忽略尾随空格。 例如:
class CustomFilterPolicy : public leveldb::FilterPolicy {
private:
FilterPolicy* builtin_policy_;
public:
CustomFilterPolicy() : builtin_policy_(NewBloomFilterPolicy(10)) {}
~CustomFilterPolicy() { delete builtin_policy_; }
const char* Name() const { return "IgnoreTrailingSpacesFilter"; }
void CreateFilter(const Slice* keys, int n, std::string* dst) const {
// Use builtin bloom filter code after removing trailing spaces
std::vector<Slice> trimmed(n);
for (int i = 0; i < n; i++) {
trimmed[i] = RemoveTrailingSpaces(keys[i]);
}
return builtin_policy_->CreateFilter(&trimmed[i], n, dst);
}
};
高级应用程序可能会提供一个不使用布隆过滤器的过滤器策略,但使用其他一些机制来汇总一组密钥。 有关详细信息,请参阅leveldb/filter_policy.h
。
Checksums(校验和)
leveldb将校验和与其存储在文件系统中的所有数据关联起来。 提供了两个单独的控件,用于验证这些校验和的激进程度:
ReadOptions :: verify_checksums
可以设置为true,以强制校验和验证代表特定读取从文件系统读取的所有数据。 默认情况下,不进行此类验证。
在打开数据库之前,可以将Options :: paranoid_checks
设置为true,以便在数据库检测到内部损坏时立即引发错误。 根据数据库的哪个部分被损坏,数据库打开时或者稍后由另一个数据库操作引发错误。 默认情况下,偏执检查是关闭的,这样即使数据库的一部分持久性存储已被损坏,数据库也可以被使用。
如果一个数据库被损坏(也许在打开偏执检查时不能打开),leveldb :: RepairDB
函数可能被用来恢复尽可能多的数据。
Approximate Sizes(预估大小)
GetApproximateSizes
方法可用于获取一个或多个键范围使用的文件系统空间的近似字节数。
leveldb::Range ranges[2];
ranges[0] = leveldb::Range("a", "c");
ranges[1] = leveldb::Range("x", "z");
uint64_t sizes[2];
leveldb::Status s = db->GetApproximateSizes(ranges, 2, sizes);
前面的调用将把sizes [0]
设置为键范围[a..c]
和sizes [1]
使用的文件系统空间的近似字节数, 键范围[x..z]
。
Environment(环境)
所有由leveldb实现发布的文件操作(和其他操作系统调用)都通过leveldb :: Env
对象进行路由。 复杂的客户可能希望提供自己的Env实施以获得更好的控制。 例如,应用程序可能会在文件IO路径中引入虚假延迟,以限制leveldb对系统中其他活动的影响。
class SlowEnv : public leveldb::Env {
... implementation of the Env interface ...
};
SlowEnv env;
leveldb::Options options;
options.env = &env;
Status s = leveldb::DB::Open(options, ...);
Porting(移植)
leveldb可以通过提供leveldb / port / port.h
输出的类型/方法/函数的平台特定实现移植到一个新的平台上。 有关更多详细信息,请参阅leveldb / port / port_example.h
。
另外,新平台可能需要一个新的默认leveldb :: Env
实现。 例如,参见leveldb / util / env_posix.h
。
utxo的刷盘逻辑主要在txdb.cpp
中实现,主要是 CoinsViewDB::batchwrite
这个函数。下面我们来分析一下:
bool CCoinsViewDB::BatchWrite(CCoinsMap &mapCoins, const uint256 &hashBlock) {
CDBBatch batch(db);
size_t count = 0;
size_t changed = 0;
for (CCoinsMap::iterator it = mapCoins.begin(); it != mapCoins.end();) {
if (it->second.flags & CCoinsCacheEntry::DIRTY) {
CoinEntry entry(&it->first);
if (it->second.coin.IsSpent()) {
batch.Erase(entry);
} else {
batch.Write(entry, it->second.coin);
}
changed++;
}
count++;
CCoinsMap::iterator itOld = it++;
mapCoins.erase(itOld);
}
if (!hashBlock.IsNull()) {
batch.Write(DB_BEST_BLOCK, hashBlock);
}
bool ret = db.WriteBatch(batch);
LogPrint("coindb", "Committed %u changed transaction outputs (out of %u) "
"to coin database...\n",
(unsigned int)changed, (unsigned int)count);
return ret;
}
在前面我们介绍过 CDBWrapper
主要是对 leveldb的一个简单封装,定义一个CDBWrapper db;
我们拿着 db 就可以实现相应的操作。
CDBWrapper.png
接下来迭代mapCoins,并填充其值,这里最主要的就是作为k-v数据库的leveldb中的key与value如何获得:
key
CoinEntry是一个辅助工具类。
struct CoinEntry {
COutPoint *outpoint;
char key;
CoinEntry(const COutPoint *ptr)
: outpoint(const_cast<COutPoint *>(ptr)), key(DB_COIN) {}
template <typename Stream> void Serialize(Stream &s) const {
s << key;
s << outpoint->hash;
s << VARINT(outpoint->n);
}
};
key指向的是outpoint,具体结构如下:
key
我们将序列化后的值当作key,作为entry的参数,同时作为db.write
的key。
关于db.write和db.WriteBatch二者之间的联系,前面已经详细分析。
value
value的值就是 coin 序列化后的值,而 coin 又包含了txout,如下:
class Coin {
//! Unspent transaction output.
CTxOut out;
//! Whether containing transaction was a coinbase and height at which the
//! transaction was included into a block.
uint32_t nHeightAndIsCoinBase;
同样的,我们进行序列化并使用CTxOutCompressor
对txout进行压缩,REF是一个宏定义,是非const转换,我们首先断言这个币是否被消费:
template <typename Stream> void Serialize(Stream &s) const {
assert(!IsSpent());
::Serialize(s, VARINT(nHeightAndIsCoinBase));
::Serialize(s, CTxOutCompressor(REF(out)));
}
txout主要包含:
class CTxOut {
public:
Amount nValue;
CScript scriptPubKey;
对nValue和scriptPubKey采用了不同的压缩方式来进行序列化,如下:
class CTxOutCompressor {
private:
CTxOut &txout;
public:
template <typename Stream, typename Operation>
inline void SerializationOp(Stream &s, Operation ser_action) {
if (!ser_action.ForRead()) {
uint64_t nVal = CompressAmount(txout.nValue);
READWRITE(VARINT(nVal));
} else {
uint64_t nVal = 0;
READWRITE(VARINT(nVal));
txout.nValue = DecompressAmount(nVal);
}
CScriptCompressor cscript(REF(txout.scriptPubKey));
READWRITE(cscript);
}
};
这时候我们就拿到了db.write
的value值,这时候我们通过for循环,不断迭代,将值写入磁盘。
链接:https://www.jianshu.com/p/833e78d79451