Hyperledger Fabric区块链结构由区块头和区块体组成,并通过父区块哈希编码构成唯一链接,Hyperledger Fabric加入一层状态缓存设计,用以提高读写性能。
Hyperledger Fabric本质上是一个分布式账本,在底层结构中都是通过键值对的方式来存储数据。而区块链1.0为了实现数据的时间可回溯性和数据防篡改机制,在链中并不保存数据的状态,而只保存对数据的变更。这就使得对某条数据的状态查询需要进行全链遍历,其查询性能很难满足一些企业级业务的性能需求,因此Hyperledger Fabric引入了“世界状态(World State)”这一概念。
Fabric 里的数据以分布式账本的形式存储。账本由一系列有顺序和防篡改的记录组成,记录包含着数据的全部状态改变。账本中的数据项以键值对的形式存放,账本中所有的键值对构成了账本的状态,也称为“世界状态”( World State )。当区块中保存某一条记录时,会同步更新对应Key的世界状态。当需要查询某个键值时,只需要查询对应的世界状态即可,而无需进行全链遍历。
世界状态是脱链保存在LevelDB/CouchDB结构中的,是一种链外附加缓存机制,世界状态的丢失并不会对区块链中的数据产生影响。
每个通道中有唯一的账本,由通道中所有成员共同维护着这个账本,每个确认节点上都保存了它所属通道的账本的一个副本,因而是分布式账本。对账本的访问需要通过链码实现对账本键值对的增加、删除、更新和查询等的操作。
账本由区块链(File System)和状态数据库(Level DB)两部分组成。如下图所示
状态数据库,记录了账本中所有键值对的当前值,相当于对当前账本的交易日志做了索引。链码执行交易的时候需要读取账本的当前状态,从状态数据库可以迅速获取键值的最新状态。
如果没有状态数据库,要获得某个键值时,需要遍历整个区块链中和该键值相关的交易,效率非常低,因此,读取状态数据库可以认为是快速定位和访问某个键值的方法。另外,当状态数据库出现故障的时候,可以通过遍历账本重新生成。
当一个区块附加到区块链尾部的时候,如果区块中的有效交易修改了键值对,则会在状态数据库中作相应的更新,这样区块链和状态数据库始终保持一致。
状态数据库原理上可以是各种键值数据库,Fabric 缺省使用的是 LevelDB ,也支持 CouchDB 的选项。CouchDB 除了支持键值数据之外,也支持 JSON 格式的文档模型,能够做复杂的查询。
数据存在节点:
/var/hyperledger/production/ledgersData
数据库与文件 | 文件路径 |
---|---|
id store数据库 | /var/hyperledger/production/ledgersData/ledgerProvider |
区块数据文件 | /var/hyperledger/production/ledgersData/chains/chains |
隐私数据库 | /var/hyperledger/production/ledgersData/pvtdataStore |
区块索引数据库 | /var/hyperledger/production/ledgersData/chains/index |
状态数据库 | LevelDB默认为/var/hyperledger/production/ledgersData/stateLeveldb CouchDB需要指定服务器地址、用户名、密码等配置参数 |
历史数据库 | /var/hyperledger/production/ledgersData/historyLeveldb |
transient隐私数据库 | /var/hyperledger/production/transientStore |
(1)chains:chains/chains下包含的mychannel是对应的channel的名称,因为Fabric是有多channel的机制,而channel之间的账本是隔离的,每个channel都有自己的账本空间。chains/index下面包含的是levelDB数据库文件,在Fabric中默认所有的数据库都是LevelDB,这个原因作者下面会讲到,DB中存储的就是我们上面说的区块索引部分。chains/chains和chains/index就是上面所说的File System和indexDB;
(2)stateLeveldb: 同样是levelDB数据库,存储的是智能合约putstate写入的数据;
(3)ledgerProvider:数据库内存储的是当前节点所包含channel的信息(已经创建的channel id 和正在创建中的channel id),主要是为了Fabric的多channel机制服务的;
(4)historyLeveldb:数据库内存储的是智能合约中写入的key的历史记录的索引地址。
order节点上账本数据库与区块数据文件路径的默认配置列表
数据库与文件 | 文件路径 |
---|---|
区块数据文件 | /var/hyperledger/production/orderer/chains |
区块索引数据库 | /var/hyperledger/production/orderer/index |
peer节点账本存储图如下
1 左边区块链是狭义的区块存储,底层是一个文件系统,区块并不是存储在数据库,而是直接存储为文件
2 右半部分的区块索引用于查询区块,将区块属性与区块位置相关联,例如通过区块hash、区块高度、交易id查询区块数据,区块索引的实现使用了内置数据库Leveldb
3 历史数据索引 启用与否取决于智能合约是否有查询历史的需求,记录某个键值在某条交易中被改变,只记录改变动作,不记录具体改变
4 状态数据库存储当前区块链上最新的数据,以键值对key-value的形式存储所有键的最新值
账本存储源码:
状态数据库: 可选leveldb、couchdb
vim /opt/gopath/src/github.com/hyperledger/fabric/core/ledger/kvledger/txmgmt/statedb/stateleveldb/stateleveldb.go
历史数据库
vim /opt/gopath/src/github.com/hyperledger/fabric/core/ledger/kvledger/history/db.go
vim /opt/gopath/src/github.com/hyperledger/fabric/common/ledger/blkstorage/blockstore.go
调用链码发起查询操作的时候,查的就是世界状态当中的值,因为查询操作不会对账本的当前状态造成任何改变。
而当调用链码发起写操作(更改,删除,增加)时,应用程序提交一个交易提案到背书节点,背书节点背书之后返回,应用程序再将交易发送给排序节点排序。之后排序节点将交易打包成区块,将区块分发给各个组织的 peer 节点。此时,如果该交易是一笔无效交易(比如验证签名失败,背书失败等等原因),那么该笔交易只会被存储到本地的交易记录中,但是不会更改账本的世界状态——即无效交易不会导致账本世界状态的改变。
另外,世界状态中的每个状态都有一个版本号,版本号供 Fabric 内部使用,并且每次状态更改时版本号都会发生递增。举个例子,其当前状态版本号都是0,当发生了一次对账本对象的写操作的时候,账本的状态就发生了变化,这样的新的世界状态就产生了,其中的状态的版本号就是1。
每当更新状态时,都会检查该状态的版本,以确保当前状态与背书时的版本相匹配。这就确保了世界状态是按照预期进行更新的,没有发生并发更新,是并发安全的。
首次创建账本时,世界状态是空的。因为区块链上记录了所有代表有效世界状态更新的交易,所以任何时候都可以从区块链中重新生成世界状态。这样一来就变得非常方便,例如,创建节点时会自动生成世界状态。此外,如果某个节点发生异常,重启该节点时能够在接受交易之前重新生成世界状态。