-
Notifications
You must be signed in to change notification settings - Fork 256
存储模块
[TOC]
和传统的区块链实现相同,chain33中使用高性能、高可靠的KV DB来进行数据的存储,并且设计上提供的DB接口也都是针对KV存储的特性定义的。
在chain33系统中,目前存在4个数据库实例,如下:
fzm@fzm001:~/chain33 $ ls datadir
addrbook blockchain.db mavltree wallet
其中:
addrbook
实现的能力比较单一,主要是存储P2P的节点以及相关的状态信息。
wallet
存储本地账号信息。
blockchain.db
存储区块头、区块体以及区块相关的附加信息,还存储交易本地执行返回的结果信息。
store
存储交易执行的结果以及区块状态哈希信息。
对于chain33的存储模块,有以下几个要点:
-
chain33的底层存储及操作通过灵活配置可支持多种实现方式。
底层根据配置可以支持多种类型的KV DB实现,比如goleveldb、gobadgerdb、gomemdb、gossdb等。
上述addrbook、wallet、blockchain.db、store这4个数据库实例的底层存储操作都是通过配置的具体的KV DB来实现的,比如默认配置的goleveldb。
-
实际应用中,Excutor执行器通过StateDB和LocalDB两个抽象数据库概念,分别用来对blockchain.db和store的存储进行消息查询等操作。
-
Store模块数据的存储格式,也采用可配置、可插拔的方式,默认支持mavl tree的存储格式。
chain33也支持用户自行扩展新的存储格式,比如单纯的KVDB存储格式、基于MVCC的KVDB存储格式、MPT存储格式等。
如1中描述,最终对数据存储的底层操作还是通过配置的具体的KV DB来实现的,比如goleveldb。
-
P2P、Wallet、Blockchain、Store几个模块均涉及数据存储,底层通过DB接口向KV DB实例写入及读出数据。
-
BlockChain及Store模块也提供数据查询接口,具体通过StateDB、LocalDB抽象数据库对象使用消息来进行操作。
-
BlockChain模块在生成区块时,可以通过消息向Store模块写入状态信息。
-
Wallet、Consensus、Client等模块可以通过执行器,由StateDB、LocalDB对象分别向Blockchain、Store使用消息发起数据查询。
-
Client模块也可以通过消息直接向Store发起状态数据查询。
底层数据存储支持的kv db都需要适配上述接口。
以goleveldb为例:
可以看到goleveldb实现了DB接口、Iterator接口、Batch接口,就可以作为存储模块的底层数据库实现来在chain33系统中被使用。
gobadgerdb、gomemdb、gossdb等不同的kv db的实现也是类似的,都需要实现DB接口、Iterator接口、Batch接口以满足上层逻辑功能的需要。
可见,这两个抽象数据库的功能非常简单,容易导致误解的是两个DB的使用场景和存储内容,所以下面使用表格方式,列出了两个DB之间的区别。
DB | StateDB | LocalDB |
---|---|---|
写数据目的 | 缓存 | 缓存 |
读数据来源 | store | blockchain.db |
执行器对应方法(数据来源) | Exec返回的KV | ExecLocal返回的KV |
数据是否包含状态 | 是(读写数据都附带StateHash) | 否(仅通过Key索引数据) |
存储哪些数据 | 存储区块交易执行的kvset(kv数据库直接存储;mavl数据库以StateHash构造树存储) | 存储区块的所有信息 |
是否校验数据 | 是(执行区块时重新计算StateHash校验) | 否(直接写入) |
- Store层决定了状态数据在底层KV DB中将以何种格式进行组织。
- 系统默认支持mavl tree格式的实现,通过加载插件还可支持单纯的KV DB的实现、基于MVCC的KV DB的实现、MPT的实现等。
- 开发者如果要扩展开发自己的数据组织格式,需要实现SubStore接口,并注册到系统中,做好对应配置即可。
数据接口定义在多个interface中,下面的介绍关注方法即可。
Get(key []byte) ([]byte, error)
BatchGet(keys [][]byte) (values [][]byte, err error)
Set(key []byte, value []byte) (err error)
SetSync([]byte, []byte) error
Delete([]byte) error
DeleteSync([]byte) error
Close()
这些就是基本的数据的读、写、删除接口,在加上关闭数据库的接口。纯粹的Key-Value操作,直接和具体数据库的接口对接即可。有些数据库可能不支持异步写操作,这时只能都按照同步实现(这个对性能影响不大)。
List(prefix, key []byte, count, direction int32) ([][]byte, error)
PrefixCount(prefix []byte) int64
这两个接口需要实现所谓的前缀查找,其实在KV的实现中,并不是纯粹的前缀查找,而是范围(Range)查找,因为匹配的数据KEY是按照根据prefix推算出来的范围进行查找的,下面举一个例子就知道了(这个查找效果和*后缀模糊匹配是同样的)。
例一: prefix=0xab9876abcd77ff
这时的查找范围是: 0xab9876abcd 77ff xxx ~ 0xab9876abcd 78ff xxx
例二:prefix=0xab9876abcd77ab
这时的查找范围是: 0xab9876abcd77 ab xxx ~ 0xab9876abcd77 ac xxx
其中的count是限定查找的个数,因为可能匹配的数据量很大,只返回前面的count个;
direction是匹配方向,正向的化就是从开始到结束,反向就是从结束到开始;
特别注意: 当count为0时(或PrefixCount接口),意思是返回所有匹配的数据,但是在具体数据库实现时,如果数据量特别大,可能会导致数据库挂死、网络超时、内存溢出等各种问题。 比较保险的做法是,内部查找时做一下处理,每次只取固定条数的记录(比如1万条),然后分多次查找,最终汇总返回结果。
type Batch interface {
Set(key, value []byte)
Delete(key []byte)
Write() error
}
可以认为这是一个事务接口,可以进行多次Set和Delete操作,最终一次Write提交操作,要么全部成功要么全部失败。
在数据库实现时,特别是不支持原生事务的数据库时,可能要考虑折中实现,下面以ssdb的实现为例说明:
- newBatch时,开辟两个缓存对象,分别用来存储修改和删除操作缓存队列;
- Set操作时,检查Delete队列是否有相应Key,如有则从Delete队列删除,Set操作入Set队列;
- Delete操作时,检查Set队列中是否有相应Key,如有则从Set队列删除,Delete操作入Delete队列;
- Write操作时,向Set队列中插入所有Delete队列中的Key,Value设置为空,然后调批量Set接口,如失败,则返回,如成功,则调批量删除接口,然后返回成功;
之所以这么操作,有两点考虑:
- 常规的事务操作是保证内部的操作顺序的,所以在Set和Delete操作时,需要检查对方的队列,防止先Delete后Set出现的问题;
- 事务提交时,为了防止写成功,但删除失败的中间状态,所以在写操作的同时,将要删除的数据置空;
type IteratorSeeker interface {
Rewind() bool
Seek(key []byte) bool
Next() bool
}
type Iterator interface {
IteratorSeeker
Valid() bool
Key() []byte
Value() []byte
ValueCopy() []byte
Error() error
Close()
}
chain33中包装的迭代器,类似C中的指针或SQL中的游标的能力,Iterator中的接口很好实现,主要是IteratorSeeker中的三个方法,对有些数据库来说,实现起来存在一定的难度。下面还是以ssdb为例,说明下迭代器的实现逻辑。
迭代器的创建接口如下:
//迭代prefix 范围的所有key value, 支持正反顺序迭代
Iterator(prefix []byte, reserver bool) Iterator
在创建迭代器时,包装了前缀查找的能力,在ssdb的实现逻辑如下:
1 使用prefix计算出匹配的开始和结束KEY; 2 使用生的keys或rkeys命令,查找满足此prefix前缀的所有KEY(使用1024进行分页),然后将这些信息包装进Iterator对象; 3 Next方法执行时,将游标+1,从缓存数组中获取对应的KEY,然后调用DB的Get接口获取数据; 3.1 如果游标+1==1024,则以当前缓存数组中的最后一个元素KEY为开始,继续获取下一页KEY; 4 Seek方法执行时,会连续调用分页获取KEYS的逻辑,然后和key匹配,直到满足,或没有数据;
// 模拟事务接口
Begin()
Rollback()
Commit()
上面三个方法是模拟事务操作,目前只在StateDB中实现,仅支持单线程内存事务,是在支持TxGroup概念的时候引入的。新增一种数据库实现时,不需要考虑对这三个接口的支持。而且在自己的代码逻辑中,也不要调用这几个接口。
SetStateDB(dbm.KV)
SetLocalDB(dbm.KVDB)
Exec(tx *types.Transaction, index int) (*types.Receipt, error)
ExecLocal(tx *types.Transaction, receipt *types.ReceiptData, index int) (*types.LocalDBSet, error)
ExecDelLocal(tx *types.Transaction, receipt *types.ReceiptData, index int) (*types.LocalDBSet, error)
交易的具体执行逻辑,不管执行了什么逻辑,最终都会返回一个*types.Receipt对象,这个对象中包含了两部分内容,KV和Logs,其中KV将会被写入StateDB(最终写入store ),而Logs将会作为入参,在调用ExecLocal时传入;
默认的Exec方法,不生成任何数据;
交易的本地执行逻辑,这是一个附加的逻辑,两次执行结果不同,不会导致区块执行失败,这里的主要逻辑一般是使用Exec生成的信息,再生成一些附加信息,方便其它地方使用;
默认的ExecLocal方法,生成交易哈希对应的交易详情数据;
相关数据最终会被blockchain写入blockchain.db。
此方法和ExecLocal对应,是在处理分叉时调用,如果一个区块已经被执行,而后另一条链成为主链,那么已经执行的区块将会被回退,这时会调用ExecDelLocal逻辑。它应该将ExecLocal写入的数据进行回滚。
//通过消息EventStoreGet从Store(对应第一章的store)中获得状态数据
Get(key []byte) ([]byte, error)
//通过消息EventLocalGet从BlockStore数据库(对应第一章的blockchain.db)中获得区块数据
Get(key []byte) ([]byte, error)
//通过消息EventLocalList从BlockStore数据库(对应第一章的blockchain.db)中查询数据列表
List(prefix, key []byte, count, direction int32) ([][]byte, error)
//通过消息EventLocalPrefixCount从BlockStore数据库(对应第一章的blockchain.db)中查询指定前缀的key的数量
PrefixCount(prefix []byte) (count int64)
type SubStore interface {
//向对应StateHash写入KV信息
Set(datas *types.StoreSet, sync bool) ([]byte, error)
//获得对应StateHash的KV信息
Get(datas *types.StoreGet) [][]byte
//Blockchain模块生成区块时,设置对应StateHash的KVSet到内存。
MemSet(datas *types.StoreSet, sync bool) ([]byte, error)
//Blockchain模块生成区块时,将对应StateHash的内存KVSet写入到存储。
Commit(hash *types.ReqHash) ([]byte, error)
//Blockchain模块生成区块失败时,将对应StateHash的内存KVSet数据清除。
Rollback(req *types.ReqHash) ([]byte, error)
//异常时,回滚已写入区块的StateHash对应的KV信息。
Del(req *types.StoreDel) ([]byte, error)
//对应于StateHash,遍历满足条件的Key、value,并使用fn函数进行处理。
IterateRangeByStateHash(statehash []byte, start []byte, end []byte, ascending bool, fn func(key, value []byte) bool)
//预留的事件处理接口
ProcEvent(msg queue.Message)
}
新增的Store层插件类型,都需要实现上述接口,比如KV DB、KVMVCC DB、MPT等。
hello world