fabric源码解析24——ledger之idStore和BlockStore

fabric源码解析24——ledger之idStore和BlockStore

概述

《fabric源码解析5》中对fabric中涉及的账本进行了初次描述,本文则着眼于更系统的描述账本在fabric中的使用。使用账本的有两处:peer结点和orderer结点。各自有自己的实现,但最最基础的底层操作还是对数据库的操作。

从数据库类型角度讲,分为goleveldbcouchDB两种,在core.yaml配置文件中ledger区域中,stateDatabase选项即为指定所选用的数据库,默认选用goleveldb。goleveldb实现在common/ledger/util下,couchDB实现在core/ledger/util下。这里的实现不是具体实现数据库,而是将涉及的第三方库的数据库对象包装进fabric自己使用的一些对象(如common/ledger/util/leveldbhelper/leveldb_helper.go中的DB对象)之中。使用couchDB,可以支持更丰富的方式进行查询。

从数据库用途角度讲,有用于存储账本ID的idStore,用于存储Block的BlockStore,有用于存储链上交易当前最新状态的VersionedDB,有用于存储有效交易部分信息的HistoryDB。

从使用者角度讲,peer结点使用的账本的基本的结构为:数据库db被包含在一个账本实例ledger中,ledger可以被一个账本提供者ledger_provider提供,ledger_provider被包裹在一个账本管理者ledger_mgmt中,ledger_mgmt供peer点的其他模块直接使用。更详细的请参看图peer_ledger.png。orderer结点使用账本的基本结构与peer结点类似,只是形成的结构有所出入而已,详情请参看图orderer_ledger.png。下文只以peer结点使用账本为例详述。

从功能角度讲,对于存储block的数据库,由于区域链不允许对链上的数据进行修改和删除,即已发生的数据永久而不变的保留,因此数据库有的增删改查操作,到了fabric用于存储block的账本中只有增查两个操作,加上数据库的开启和关闭两个操作,剩余的就是fabric账本针对区域链而保有的一些辅助功能。

涉及的目录

  • common/ledger
  • core/ledger
  • orderer/ledger
  • protos/ledger

peer结点中的leveldb

peer结点使用github.com/syndtr/goleveldb/leveldb这个leveldb这个库,是将leveldb的数据库对象和操作都封装进自己的对象中进行使用,在common/ledger/util/leveldbhelper/下,主要有三个对象:(1)DB,在leveldb_helper.go中,是一个数据库对象,封装了配置conf、leveldb.DB、同步或非同步的读写选项。(2)DBHandle,在leveldb_provider.go中,是一个数据库处理对象,直接封装了DB和该数据库名称,也是直接由账本使用的对象,有PutGetDelete这样的基础操作。(3)Iterator,在leveldb_provider.go中,数据库迭代器,直接封装了leveldb的Iterator,也没有特别之处。

peer结点中的账本

基础功能:创建,开启,查,增,关闭。

辅助功能:在各个账本章节中详述。

数据库:idStore,BlockStore,VersionedDB,HistoryDB。

peer使用的账本的直接对象是core/ledger/kvledger/kv_ledger.go中的kvLedger对象,除了idStore被更高层的PeerLedgerProvider管理外,其余三个账本均由kvLedger持有管理。本文将对idStore和BlockStore详述,VersionedDB和HistoryDB则在下一篇文章中详述。

创建

  1. peer channel join -b gensisblock.block命令,根据gensisblock,创建一个channel。该命令最终会触发以下过程。
  2. Invoke(stub) -> joinChain(cid, block)(core/scc/cscc/configure.go),cid即为channel的ID(这个channel的ID在后文实际上就用作账本的ID了),block即为gensisblock。
  3. peer.CreateChainFromBlock(block)(core/peer/peer.go) -> l, err = ledgermgmt.CreateLedger(cb)(core/ledger/ledgermgmt/ledger_mgmt.go) -> ledgerProvider.Create(genesisBlock)(core/ledger/kvledger/kv_ledger_provider.go)-> …,根据gensisblock创建账本并保存到了ledger_mgmt.go中的openedLedgers映射中。这一步账本被创建。
  4. 还是在peer.CreateChainFromBlock(block)中,在创建完账本l后,会执行createChain(cid, l, cb) -> c := committer.NewLedgerCommitterReactive(ledger,...),创建了一个**账本提交者对象**c,而生成的账本l被c持有。账本提交者对象为core/committer/committer_impl.go中的LedgerCommitter,在peer中向账本提交数据或获取账本信息,均是通过该对象的Commit(block)接口提交给账本的。committer.go中接口的注释中的一句话可以诠释该提交者的目标:leave-everything-to-the-committer-for-now,即现在把所有的事情交给committer。
  5. 还是在createChain(cid, l, cb)中,创建了账本提交者对象c后,会执行service.GetGossipService().InitializeChannel(...,c,...) -> state.NewGossipStateProvider(...,committer,...) -> …,c被传到gossip模块的state实例中,被state持有。
  6. 这里还有一点可说的是,主要是针对第3点创建账本的,账本有创建和打开两个接口,分别对应core/ledger/kvledger/kv_ledger_provider.go中的Create(genesisBlock)Open(ledgerID)两个实现。创建包含了打开,是在账本不存在时进行的操作,创建的时候会将gensisblock直接写入账本。打开则是在账本已经存在时进行的操作。一个账本存在的标准:idStore中存有该账本的ID 且 该账本的在建标识不存在。关于在建标识下文会详述。

使用

回看《fabric源码解析14》state模块章节。在peer结点接收由orderer结点发送来的block数据后(这个过程下文省略),会交由gossip模块中的state子模块向账本添加block。而state添加时,则是通过账本提交者对象向账本添加block的。具体过程如下,在gossip/state/state.go中:

  1. … -> deliverPayloads() -> s.commitBlock(rawBlock) -> s.committer.Commit(block),这里的committer即为上文第4、5点创建后被传入state模块并由state模块持有的**账本提交者对象**c。该文件其他函数中还有调用committer.LedgerHeight(),即利用账本提交者对象获取账本对象。
  2. 当block一路向账本提交,会到达core/ledger/kvledger/kv_ledger.go中的Commit(block)处,在这个函数中:
  3. l.txtmgmt.ValidateAndPrepare(block, true),验证并准备block数据,为向VersionedDB中写入做准备。
  4. l.blockStore.AddBlock(block),向BlockStore账本中写入block。
  5. l.txtmgmt.Commit(),在第3步的准备下,这里将block数据写入VersionedDB。
  6. l.historyDB.Commit(block),如果if ledgerconfig.IsHistoryDBEnabled(),即HistoryDB配置使能,则也将block数据写入HistoryDB账本。
  7. 这里需注意一下,虽说三个账本都是写入block数据,但是写入的数据各有不同,具体写何内容在下文各个账本章节中详述。

idStore

idStore相对简单,对象为core/ledger/kvledger/kv_ledger_provider.go中的idStore,直接使用了leveldb。主要有两个功能:(1)存储创建的账本的ID。(2)使用一个在建标识(ConstructionFlag)来标记账本是否正确建立,类似于一个检查点。

存储账本ID

存储账本ID相当简单,只有增/查操作,增,以 ledgerKeyPrefix+账本ID 为key,以gensisblock为value,组成键值对进行存储。查,有ledgerIDExistsgetAllLedgerIds两个接口,前者判断一个账本是否存在,后者获取所有建立成功的账本ID。

ConstructionFlag

建立一个账本需要一定的时间,而若在建立账本过程中系统崩溃,则会出现账本部分建立的情况,leveldb中可能残留部分账本数据,磁盘中可能残留部分账本文件,这些需要清除或修复,否则可能会影响账本的再次建立(实际上不影响,但是可以做,具体参看kv_ledger_provider.go中的runCleanup(...))。idStore中以underConstructionLedgerKey为key,账本ID为value,组成键值对进行存储,从而作为一个在建标识,标记这个账本正在创建当中。在kv_ledger_provider.go中:

  1. Create(genesisBlock)创建账本时,在检查完该账本是否存在后,若账本不存在,则立即provider.idStore.setUnderConstructionFlag(ledgerID),向idStore中添加在建标识,对当前创建的账本进行标记。
  2. provider.openInternal(ledgerID)ledger.Commit(genesisBlock),先创建账本,这需要一些列的操作,然后再向账本中存储genesisBlock。
  3. provider.idStore.createLedgerID(ledgerID, genesisBlock),在这个函数中,batch.Put(key, val)向idStore中存储该账本ID,batch.Delete(underConstructionLedgerKey)从idStore中删除在建标识。最后s.db.WriteBatch(batch, true)同步的批量写入这两点改变,如此这般这个账本才算完整的建立起来。而在WriteBatch将在建标识真正从idStore中删除前任何时候系统发生崩溃,下次系统重启时读取idStore时,在建标识仍能被读取出来,因此在建标识可以标识账本只被部分在建的情况,此时需要执行recoverUnderConstructionLedger来恢复账本。

账本恢复

由ConstructionFlag标志标识着最新的账本是否成功建立,这里分为两种情况,即gensisBlock是否成功写入。在创建账本管理对象PeerLedgerProvider时,会主动尝试恢复一下账本(参看kv_ledger_provider.go中的NewProvider())。在recoverUnderConstructionLedger()中:

  1. ledgerID, err := provider.idStore.getUnderConstructionFlag()获取当前idStore中的在建标识,若不存在,则ledgerID == ""成立,说明当前不存在不完整在建的账本,程序返回。否则会继续执行,实际进行账本的恢复工作。
  2. ledger, err := provider.openInternal(ledgerID),再次创建VersionedDB,BlockStore,HistoryDB。bcInfo, err := ledger.GetBlockchainInfo(),从BlockStore中读取block账本信息(包括当前账本高度,当前账本最新存储的block的哈希值,上一块block的哈希值)。
  3. switch bcInfo.Height { ... },根据获取的账本高度,分两种情况:(1)case 0:说明gensisblock未完整写入,则执行provider.runCleanup(ledgerID)provider.idStore.unsetUnderConstructionFlag(),即清理后删除在建标识。这说明两点,一是block序列号确实是从0开始的,二是在未写入gensisblock的情况下账本算作未建立。(2)case 1:说明gensisblock已写入,则执行genesisBlock, err := ledger.GetBlockByNumber(0)provider.idStore.createLedgerID(ledgerID, genesisBlock),获取gensisblock块,并据此在idStore中添加这个账本。
  4. 这里需要注意的是:这里的账本恢复是在账本级别的层面上进行的恢复,而每个账本在创建时进行的各种操作因系统崩溃而造成的部分写入的情况,都由各自账本自己负责恢复或清除(这将在讲其他账本时详述)。而且在此基础上可以说的是,恢复工作的重点是修复和同步数据,而不在于创建直接可以使用的账本对象(创建可以使用的账本对象由其他接口负责,如kv_ledger_provider.go中的Create(genesisBlock)Open(ledgerID)),因此第2点所新创建的各个账本对象,只是为了让这些账本做一下自己所负责的恢复工作而已,这一点可以从第2点bcInfo, err := ledger.GetBlockchainInfo()之后就执行了ledger.Close()将创建的账本对象关闭的操作看出来。

BlockStore

BlockStore是一个较底层的,基础的,公用的,存储block块数据对象的接口,具体实现为common/ledger/blkstorage/fsblkstorage/blockfile_mgr.go中的blockfileMgr,统筹管理block的存储操作。block的存储状态信息block数据自身是分开存储的:当前block块的存储状态存储在leveldb数据库中,block块数据自身则存储在称为blockfile的文本文件中。换句话说,leveldb用于存储当前账本的block数据的保存状态信息,这些信息主要指block的位置,长度信息,当前blockfile文件大小,index信息等,主要是用以定位block在blockfile中的所在位置,而blockfile文件保存实际的block数据。基本的操作方式也是根据leveldb中的信息去blockfile中对block进行读写操作,两者相互配合,共同保证block存储的完整性。另外有一个block迭代器对象,用于逐个读取block块数据的对象。

这里涉及到几个对象(以下所说的block均指账本中当前存在的最新的block,目录以common/ledger/blkstorage/fsblkstorage/为基准):

blockfile

存储block块数据的文件。具体实现为block_stream.go中的blockStream,主要对blockfile进行读写操作。blockfile文件名以blockfile为前缀,以六位数字为后缀,依次增加,如blockfile_000000,blockfile_000001,…。

首先说一下block数据在blockfile中具体的存储方式,存储方式决定了如何具体的读取和写入block,所以应先讲。一个完整的block数据包包含两部分:block数据长度信息+block数据自身,这里分别用A和B表示,block数据包在blockfile中的偏移也是以A为开始算起的(该例子下文依旧使用)。如一块block数据为blockBytes,则其长度是一个uint64类型的整数为blockBytesLen = uint64(len(blockBytes)),为了节约空间和方便读取,会用blockBytesEncodedLen := proto.EncodeVarint(blockBytesLen)对这个整数编码形成一个变长的数据(原理是在大端系统中删除一个整数低位部分的0)。这里的blockBytesEncodedLen就是A,blockBytes就是B。然后A和B先后写入blockfile后,这块block数据算是写入完成。注意,A是代表了B的长度的数据,A本身也有个长度,即len(A)+len(B)才是这个block数据包的长度。其他的对象,比如保存点checkpointInfo,会在写入的时候记录下该块block在blockfile中的位置等信息。

从blockfile中读取B时,会直接在block数据包的开始处(也就是从A开始)直接读取8个字节(A的所在的位置由leveldb存储的checkpointInfo保存),然后调用length, n := proto.DecodeVarint(lenBytes)尝试解码A的实际值。这里直接读取8个字节,是假设这8个字节中一定会完整包含A,笔者实验了一下,对一个512万亿的数字进行EncodeVarint,得到的数据的长度也只有7位,也就是说,这里假设一个block的大小值被EncodeVarint后的数据一定<=8位。另外,如当A只有3位,读取的8位数据的剩余5位为B的数据时,DecodeVarint依然会解码出A来,此时返回的n为3,length则为A代表的实际值(也就是B的实际大小)。然后,根据解码出来的length,以及A所占的n个字节,则可以定位到B开始的地方,然后读取length个字节,也就完整的把B读取出来了。对于B是否完整,是通过比较当前blockfile的大小与length+n+checkpointInfo记录的block数据包的偏移值之和的值来判定的,若后者小于当前blockfile的实际大小值,则自然是完整的。

读取block数据包,是由block_stream.go中的blockStreamblockfileStream对象完成的。其中能体现上述读取过程的,是两个对象的nextBlockBytesAndPlacementInfo接口。该接口每次从指定的文件中的指定位置开始,尝试读取一个block数据包。

block信息数据库

存储当前账本中block存储状态信息的leveldb数据库,具体到blockfileMgr对象中的dbindex两个成员(两者实际上使用的是同一个 leveldb数据库)。主要存储两种数据:(1)检查点,checkpoint,具体为blockfile_mgr.go中的checkpointInfo对象。每在blockfile中存储一个block数据包,都会在leveldb中存储该block数据包对应的检查点。检查点以blkMgrInfoKey为key(blockfile_mgr.go中),checkpointInfo为value,组成一个键值对存储在leveldb中。latestFileChunkSuffixNum记录block所在的blockfile的文件名后缀,latestFileChunksize记录当前blockfile最新的大小,isChainEmpty标识当前账本是否为空,lastBlockNumber记录账本当前最新block的序列号。(2)索引,index,具体为blockindex.go中的blockIndex对象。每在blockfile中存储一个block数据包,都会在leveldb中存储该block数据包对应的一批索引。索引分别以block序列号,block哈希值,交易ID等为key,以账本中最新block的位置信息等为value,组成一批键值对,存储在leveldb中。这些索引项的key值预定义在common/ledger/blkstorage/blockstorage.go中,主要是为调用者提供多种的索引方式去在blockfile中定位block块数据,比如有的想用block的序列号去查找一个block数据,有的想使用block数据的哈希值去查找block数据,有的想使用交易ID去查找一个block中的具体交易的数据,等等。block数据包对应的每批索引中,包含了block块中每笔具体交易数据的索引,具体存储在blockIndex对象的txOffsets,这个成员在计算每笔交易在blockfile中的偏移位置时,随着添加A+B,总共经历了三轮,首先计算的是每笔交易在B中的相对偏移,然后计算的是每笔交易在A+B中的相对偏移,最后计算的是每笔交易在blockfile中的偏移。索引自身也有一个检查点indexcheckpoint,用于记录当前最新的block数据包的索引的存储情况。

在写block数据包时:当先后写入A和B后,(1)创建一个新的checkpointInfo对象newCPInfo,然后根据现有的checkpointInfo对象currentCPInfo和写入的block数据包填充newCPInfo,再非同步的调用db.Put(...)将新的checkpointInfo写入leveldb中,覆盖掉currentCPInfo。检查点的作用主要在于记录当前账本的保存状态,也就是说记录账本当前保存到哪儿了,保存到哪个block了。(2)调用indexBlock(...),进而调用batch.Put(...),以批量写入的形式将各个索引对应的信息写入leveldb中,因为每批索引所使用的key不会一样,因此不会发生覆盖。索引检查点会在每批的索引的最后写入,也即当索引检查点存在,则当批索引一定存在。索引的作用主要在于为调用者提供多种查找具体block或交易数据的方式。

在此总结一下,写入一个block,从前到后所涉及的要依次写入的每个数据:

  1. block长度信息
  2. block块数据
  3. blkMgrInfoKey - checkpointInfo
  4. blockHashIdxKeyPrefix+block哈希值 - block数据包位置信息
  5. blockNumIdxKeyPrefix+blockID - block数据包位置信息
  6. txIDIdxKeyPrefix+交易ID - 交易数据位置信息(每个block中所含的所有交易)
  7. blockNumTranNumIdxKeyPrefix+blockID+交易Seq - 交易数据位置信息(每个block中所含的所有交易)
  8. blockTxIDIdxKeyPrefix+交易ID - block数据包位置信息(每个block中所含的所有交易)
  9. txValidationResultIdxKeyPrefix+交易ID - 交易验证结果(每个block中所含的所有交易)
  10. indexCheckpointKey - block序列号

上述列表中,可划分出两个范畴,三个小整体:1和2存储到blockfile文件中,属于一个范畴,是一个小整体;3-9存储在leveldb中,均为键值对(key-value pair),属于一个范畴;3为检查点键值对,是一个小整体;4-10为索引键值对,10是索引检查点,是一个小整体。blockID指block的序列号,交易ID指交易的序列号,交易Seq指交易在block的Data数组中的下标序号。所有的key的…Prefix前缀均在blockindex.go中定义。3和10的每次更新均会覆盖旧的值。

block数据包或交易数据的位置信息是一个blockindex.go中的fileLocPointer对象,该对象中,fileSuffixNum记录数据所在的blockfile文件后缀,offset记录数据在blockfile文件中的偏移,也就是数据从哪个位置开始的,bytesLength记录数据的大小(这个字段,block数据包未使用,因为其长度存放在A中,没必要使用这个字段。而交易数据使用了这个字段,来记录每个交易数据的大小)。

同时,正因为在写入一个block时涉及到多个、多类数据,并先后写入,因此存在当系统崩溃时数据部分写入和三个小整体数据不同步的情况。也因此在重新启动系统建立新的blockfileMgr时,需要对部分写入的数据进行修补和对三个小整体的数据进行同步。同步时,遵循的一个隐含的规则是前面的数据未写入,则后边的数据肯定没有写入,存在的情况有:(1)1未完整写入,(2)1完整写入,2未完整写入,(3)1-2完整写入,3未完整写入,(4)1-3完整写入,4-10未批量写入。据此,同步的步骤如下:

  1. 从leveldb中获取存有的检查点信息cpInfo(如果没有则说明是新建的账本)。
  2. 根据cpInfo中存储的block数据包的位置信息,定位该block数据包在哪个blockfile中的哪个位置。然后以此为开始尝试读取一个完整的block数据包(读取方法在上文blockfile章节已经叙述过),对cpInfo进行更新同步。
  3. 这里需要辨别的是(1)-(3)三种情况,这三种情况下,取出的cpInfo其实是上一个block数据包对应的cpInfo,定位读取的时候也会完整读取出上一个block数据包,然后再读取到此block时,当是情况(1)(2)时,由于当前block数据包数据未完整写入,因此会直接从上一个block数据包的末尾处截断blockfile文件,删除当前block数据包不完整的数据,此时也不需要再更新cpInfo;当是情况(3)时,由于当前block数据包数据已完整写入,因此会再读取当前block数据包,依据当前block数据包的信息,更新cpInfo和索引(索引会使用更新过后的cpInfo进行更新)。当是情况(4)时,取出的cpInfo为当前block数据包对应的cpInfo,cpInfo不用更新,而只需要对索引信息进行同步。索引同步时,会读取索引的检查点信息,该索引检查点信息必然是上一个block数据包的索引检查点,而由此获取的block数据包位置信息也是上一块的block,跳过该block后,就同cpInfo更新的方式类似。

block迭代器

迭代器自身没有太多可说的,依旧是调用者给一个从何处开始的参数,然后调用Next()依次遍历。当所要读取的block序列号大于账本中最新的block序列号时,迭代器会等待,直到新的block块产生并成功写入blockfile文件中。

创建-增-查

这里在代码中追溯一下BlockStore的主要操作,细节部分(如偏移量如何计算确定等)未细说。以下涉及的代码的基准目录为common/ledger/blkstorage/fsblkstorage/,涉及到的函数若为表明所在位置,请自行搜索。

创建

  1. blockfile_mgr.go中,newBlockfileMgr(...)创建了一个新的block存储管理者,接收了配置,索引配置和一个leveldb数据库三个参数。其中配置中包含了block存储路径等信息。leveldb以参数的形式传入,给了更高层的管理对象,如fs_blockstore.go中的fsBlockStore,fs_blockstore_provider.go中的FsBlockstoreProvider,更大的灵活性。
  2. cpInfo, err := mgr.loadCurrentInfo(),即是从leveldb中取出最新的检查点数据。当if cpInfo == nil,则说明当前新建的block存储管理者是管理的一个比较新的账本,要么什么数据都未写入,要么写入了第一块block数据包的部分数据(参看上文数据部分写入的情况(1)-(3)),则可以将cpInfo更新为blockfile文件开始处(以供后续步骤进行尝试读取现有的block数据,然后更新cpInfo)。
  3. syncCPInfoFromFS(rootDir, cpInfo),即根据取出来的cpInfo同步更新的函数。在这个函数中,scanForLastCompleteBlock(...)是尝试读取最后blockfile最后一块block的函数,所给的三个参数分别是哪个目录,哪个文件(后缀),从文件里的哪个位置开始读,第三个参数赋的值为int64(cpInfo.latestFileChunksize),即cpInfo所对应的block数据包写入后文件的大小,也就是从cpInfo所代表的block数据包末尾的下一个位置(即下一个block数据包开始的位置)开始尝试读取一个完整的block数据包,如此,至多读取出一个完整的block。读取的方法依然如上文所述,具体的实现是block_stream.go中的nextBlockBytesAndPlacementInfo()。若读取了这个至多的一个新的完整的block数据包,则会更新cpInfo的latestFileChunksize(指向blockfile中最新的block数据包尾),lastBlockNumber(更新为最新block的序列号),isChainEmpty(更新为账本非空)三个字段。
  4. currentFileWriter.truncateFile(cpInfo.latestFileChunksize),根据更新过后的cpInfo所存储的最后一个block数据包的末尾位置,直接截断该blockfile文件。这比较好理解,更新过后的cpInfo的latestFileChunksize值之后的数据都肯定是不完整的,所以直接截断,清除不完整的数据。
  5. mgr.syncIndex(),根据更新过后的cpInfo和自身的检查点信息,创建同步索引。在这个函数中,lastBlockIndexed, err = mgr.index.getLastBlockIndexed()即为获取索引的检查点,获取的是解码后的block序列号,也即表示序列号为lastBlockIndexed的block数据包的索引已经完整存储,同样,若未获取,则说明所处理的账本较新,还没有索引存储进leveldb中。flp, err = mgr.index.getBlockLocByBlockNum(lastBlockIndexed)是使用blockNumIdxKeyPrefix+blockID的索引方式获取序号为lastBlockIndexed的block数据包位置信息flp。又因为能成功获取索引的检查点,所以可知该block数据包一定是完整存储的,因此可以直接跳过这个block数据包而去尝试读取下一个block,这也是函数中skipFirstBlock变量所起到的作用。而一旦能成功读取下一个block数据包,则会调用info, err := extractSerializedBlockInfo(blockBytes)根据读取出来的block数据包创建新的索引信息,然后调用mgr.index.indexBlock(blockIdxInfo)将新的block数据包的索引写入leveldb中,从而完成索引的更新同步。

  1. addBlock(block)对一个block数据块进行了添加。在这个函数中,if block.Header.Number != mgr.getBlockchainInfo().Height首先验证了添加block数据的序列号是否是下一个block应有的,即序列号是依次连续的。blockBytes, info, err := serializeBlock(block)将block数据串行化成可以直接写入blockfile文件中的数据,这里的blockBytes即为B,也在这里第一次计算了每笔交易数据在block块内的偏移位置(即以block.Data为开始每笔交易的偏移)。currentOffset := mgr.cpInfo.latestFileChunksize即为当前blockfile的大小,也就是准备开始写当前添加的block数据包的位置。blockBytesLen := len(blockBytes)blockBytesEncodedLen := proto.EncodeVarint(uint64(blockBytesLen))压缩了block的大小值,这里的blockBytesEncodedLen即为AtotalBytesToAppend := blockBytesLen + len(blockBytesEncodedLen)则为block数据包的总大小。如果if currentOffset+totalBytesToAppend > mgr.conf.maxBlockfileSize,即当前文件的大小+要添加的block数据包的大小之和大于配置中规定的blockfile的大小限制,则会调用mgr.moveToNextFile()直接新启用下一个blockfile文件来存储这个新的block数据包。
  2. mgr.currentFileWriter.append(blockBytesEncodedLen, false),非同步的在第一步确定的blockfile文件和文件中的位置处的写入A,第二个参数给的是false,因此此时A只是在缓存中而已。mgr.currentFileWriter.append(blockBytes, true),第二个参数是true,同步写入B,即连同A一同实际的写入blockfile磁盘文件中。需要注意的是,虽然这里A和B是一起写入的,但只是为了减少磁盘文件的操作,第二个参数为true,在append(...)函数(blockfile_rw.go中)中会手动调用file.Sync(),即手工刷新缓存将缓存中的数据写入实际的磁盘文件,但在这个操作也不是事务性的,即在系统层面也是一个字符一个字符向磁盘文件中写的,因此如果在写的过程中发生系统崩溃,仍会出现A或B部分写入的情况。如果写入A,B的过程中出错,则mgr.currentFileWriter.truncateFile(mgr.cpInfo.latestFileChunksize)直接把文件从最开始写的地方截断,即将可能新写入的部分block数据包的数据删去,然后添加程序结束并返回一个错误。
  3. newCPInfo := &checkpointInfo{...},根据写入的block数据包和现有检查点的信息,创建一个新的检查点。然后mgr.saveCurrentInfo(newCPInfo, false),非同步的将新的检查点写入leveldb数据库中。同样的,若是检查点添加错误,也是直接截断文件,删除写入的block数据包,将blockfile恢复如初,结束添加返回一个错误。
  4. blockFLP := &fileLocPointer{...},根据新的检查点newCPInfo和开始写入block数据包时的偏移信息,创建一个block数据包的位置信息blockFLP。for ... {txOffset.loc.offset += len(blockBytesEncodedLen)},这里第二次计算block中包含的每笔交易的偏移,即加上了A所占的字节数,计算每个交易从A处开始算起的偏移量,也可以说是每笔交易在block数据包内的偏移。mgr.index.indexBlock(...),将收集的索引使用到的关于新的block数据包的信息传入,创建新的一个block数据包的索引,在这个函数中,会逐个写入1-6的索引项,其中在添加索引3和4(上文数据6和7)这两个与交易有关的索引时,会调用txFlp := newFileLocationPointer(...),第三次计算每个交易的偏移信息,即确定每笔交易在blockfile文件内的偏移,同时也会在txFlp中记录每笔交易的大小。在所有索引项都batch写入后,然后batch.Put(indexCheckpointKey, encodeBlockNum(blockIdxInfo.blockNum))写入索引的检查点,最后index.db.WriteBatch(batch, false)非同步的写入leveldb数据库中。这里的非同步和第3点写入leveldb时都是用了非同步写入,即执行后可能这些数据是写入leveldb的缓存中而非数据库中,这样可以提高写入的效率。
  5. mgr.updateCheckpoint(newCPInfo),这个主要更新了blockfileMgr对象保存的检查点cpInfo,然后调用了一个mgr.cpInfoCond.Broadcast()函数,这个函数主要使用sync.Cond的特性,服务于通知block迭代器的Next()可能存在的等待,让其继续执行。上文已经说过,迭代器的Next()在遍历到序列号大于账本当前已有的最新block数据包的序列号时,会进入等待,一直到新的block数据包成功存储到blockfile中后,所以这里就是在成功添加了一个block数据包之后进行的通知。mgr.updateBlockchainInfo(blockHash, block),这个函数使用atomic.Value的特性,原子性的存储链(也就是账本)的信息,即一个BlockchainInfo对象,该对象Height保存了链的高度,CurrentBlockHash保存了当前链存储的最新的block的哈希值,PreviousBlockHash保存了上一个block的哈希值。

关于查询block数据很简单,不同索引所提供了不同的查询方式,均集中在blockfile_mgr.go,是一系列retrieve__By__格式的函数,名字很好理解,第一个空是你要查的东西,第二个空是你根据什么查这个东西,即通过什么来检索什么,也即By什么来retrieve什么。这些函数主要通过leveldb中保存的block数据包的索引来获取block数据。

以上即是对idStore和BlockStore的详述,下篇文章将对更复杂的VersionedDB和HistoryDB进行详述。

猜你喜欢

转载自blog.csdn.net/idsuf698987/article/details/78954946