# 存储模块

## 1 模块介绍

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

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

> fzm[@fzm001](https://github.com/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 存储模块上下文

![存储模块上下文](../../assets/s2.png)

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接口](../../assets/s3.png)

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

以goleveldb为例：

![goleveldb](../../assets/s4.png)

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

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

### 2.3 StateDB和LocalDB的逻辑架构

![StateDB和LocalDB的逻辑架构](../../assets/s5.png)

![chain33-statedb](../../assets/s6.png)

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

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

### 2.4 Store的逻辑架构

![Store的逻辑架构](../../assets/s7.png)

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