Skip to content

存储模块

linj edited this page Nov 23, 2022 · 1 revision

存储模块

[TOC]

1 模块介绍

和传统的区块链实现相同,chain33中使用高性能、高可靠的KV DB来进行数据的存储,并且设计上提供的DB接口也都是针对KV存储的特性定义的。

在chain33系统中,目前存在4个数据库实例,如下:

fzm@fzm001:~/chain33 $ ls datadir
addrbook blockchain.db mavltree wallet

其中:

addrbook

实现的能力比较单一,主要是存储P2P的节点以及相关的状态信息。

wallet

存储本地账号信息。

blockchain.db

存储区块头、区块体以及区块相关的附加信息,还存储交易本地执行返回的结果信息。

store

存储交易执行的结果以及区块状态哈希信息。

对于chain33的存储模块,有以下几个要点:

  1. chain33的底层存储及操作通过灵活配置可支持多种实现方式。

    底层根据配置可以支持多种类型的KV DB实现,比如goleveldb、gobadgerdb、gomemdb、gossdb等。

    上述addrbook、wallet、blockchain.db、store这4个数据库实例的底层存储操作都是通过配置的具体的KV DB来实现的,比如默认配置的goleveldb。

  2. 实际应用中,Excutor执行器通过StateDB和LocalDB两个抽象数据库概念,分别用来对blockchain.db和store的存储进行消息查询等操作。

  3. Store模块数据的存储格式,也采用可配置、可插拔的方式,默认支持mavl tree的存储格式。

    chain33也支持用户自行扩展新的存储格式,比如单纯的KVDB存储格式、基于MVCC的KVDB存储格式、MPT存储格式等。

    如1中描述,最终对数据存储的底层操作还是通过配置的具体的KV DB来实现的,比如goleveldb。

2 逻辑架构及上下文

2.1 存储模块上下文

存储模块上下文

  1. P2P、Wallet、Blockchain、Store几个模块均涉及数据存储,底层通过DB接口向KV DB实例写入及读出数据。

  2. BlockChain及Store模块也提供数据查询接口,具体通过StateDB、LocalDB抽象数据库对象使用消息来进行操作。

  3. BlockChain模块在生成区块时,可以通过消息向Store模块写入状态信息。

  4. Wallet、Consensus、Client等模块可以通过执行器,由StateDB、LocalDB对象分别向Blockchain、Store使用消息发起数据查询。

  5. Client模块也可以通过消息直接向Store发起状态数据查询。

2.2 底层存储KV DB的逻辑架构

DB接口

底层数据存储支持的kv db都需要适配上述接口。

以goleveldb为例:

goleveldb

可以看到goleveldb实现了DB接口、Iterator接口、Batch接口,就可以作为存储模块的底层数据库实现来在chain33系统中被使用。

gobadgerdb、gomemdb、gossdb等不同的kv db的实现也是类似的,都需要实现DB接口、Iterator接口、Batch接口以满足上层逻辑功能的需要。

2.3 StateDB和LocalDB的逻辑架构

StateDB和LocalDB的逻辑架构

chain33-statedb

可见,这两个抽象数据库的功能非常简单,容易导致误解的是两个DB的使用场景和存储内容,所以下面使用表格方式,列出了两个DB之间的区别。

DB StateDB LocalDB
写数据目的 缓存 缓存
读数据来源 store blockchain.db
执行器对应方法(数据来源) Exec返回的KV ExecLocal返回的KV
数据是否包含状态 是(读写数据都附带StateHash) 否(仅通过Key索引数据)
存储哪些数据 存储区块交易执行的kvset(kv数据库直接存储;mavl数据库以StateHash构造树存储) 存储区块的所有信息
是否校验数据 是(执行区块时重新计算StateHash校验) 否(直接写入)

2.4 Store的逻辑架构

Store的逻辑架构

  1. Store层决定了状态数据在底层KV DB中将以何种格式进行组织。
  2. 系统默认支持mavl tree格式的实现,通过加载插件还可支持单纯的KV DB的实现、基于MVCC的KV DB的实现、MPT的实现等。
  3. 开发者如果要扩展开发自己的数据组织格式,需要实现SubStore接口,并注册到系统中,做好对应配置即可。

3 处理逻辑

3.1 底层KV DB的主要接口

数据接口定义在多个interface中,下面的介绍关注方法即可。

3.1.1 基本数据接口

    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操作,直接和具体数据库的接口对接即可。有些数据库可能不支持异步写操作,这时只能都按照同步实现(这个对性能影响不大)。

3.1.2 范围查找接口

    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万条),然后分多次查找,最终汇总返回结果。

3.1.3 批量数据接口

    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接口,如失败,则返回,如成功,则调批量删除接口,然后返回成功;

之所以这么操作,有两点考虑:

  1. 常规的事务操作是保证内部的操作顺序的,所以在Set和Delete操作时,需要检查对方的队列,防止先Delete后Set出现的问题;
  2. 事务提交时,为了防止写成功,但删除失败的中间状态,所以在写操作的同时,将要删除的数据置空;

3.1.4 迭代器接口

    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匹配,直到满足,或没有数据;

3.1.5 其它特定接口

    // 模拟事务接口
    Begin()
    Rollback()
    Commit()

上面三个方法是模拟事务操作,目前只在StateDB中实现,仅支持单线程内存事务,是在支持TxGroup概念的时候引入的。新增一种数据库实现时,不需要考虑对这三个接口的支持。而且在自己的代码逻辑中,也不要调用这几个接口。

3.2 StateDB和LocalDB的相关主要接口

3.2.1执行器中,和数据存储相关的接口:

    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)
Exec

交易的具体执行逻辑,不管执行了什么逻辑,最终都会返回一个*types.Receipt对象,这个对象中包含了两部分内容,KV和Logs,其中KV将会被写入StateDB(最终写入store ),而Logs将会作为入参,在调用ExecLocal时传入;

默认的Exec方法,不生成任何数据;

ExecLocal

交易的本地执行逻辑,这是一个附加的逻辑,两次执行结果不同,不会导致区块执行失败,这里的主要逻辑一般是使用Exec生成的信息,再生成一些附加信息,方便其它地方使用;

默认的ExecLocal方法,生成交易哈希对应的交易详情数据;

相关数据最终会被blockchain写入blockchain.db。

ExecDelLocal

此方法和ExecLocal对应,是在处理分叉时调用,如果一个区块已经被执行,而后另一条链成为主链,那么已经执行的区块将会被回退,这时会调用ExecDelLocal逻辑。它应该将ExecLocal写入的数据进行回滚。

3.2.2 StateDB的主要接口

     //通过消息EventStoreGet从Store(对应第一章的store)中获得状态数据
     Get(key []byte) ([]byte, error)  

3.2.3 LocalDB的主要接口:

     //通过消息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)

3.3 SubStore的主要接口

    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等。

Clone this wiki locally