# 一、关于MVCC机制的简要说明 #
## 1.以太坊中的StateDB  
Store模块的Mavl Tree和以太坊的StateDB的实现方式比较类似：  
以太坊的StateDB在设计上并不会每生成一个块的时候都复制一份，而是重用树节点，在新块生成的时候，只改变发生状态变化的节点（通过写dirty标记），然后引用原来未变化的节点，重新计算变化节点到根路径上所有节点的哈希，即相当于生成一颗新树（State Root Hash发生了变化）：  
![Store接口时序](./resource/StateDB.png)
有了StateDB的这种设计，在区块链代码中的业务实现逻辑就会非常简单，如果需要修改账户数据（余额变更、新增合约、废弃合约等），直接操作内存中的StateDB即可，在块生成之前，系统会根据变更的信息，重新生成哈希，并将变更写到数据库。

StateDB的这种设计使用起来非常方便，但是也存在一定的问题：  
（1）因为它保存了所有的数据，随着时间的增长，这棵树会越来越大，占用越来越多的磁盘空间。
（2）对于历史数据，需要根据特定区块的State Root Hash建立起树形结构，并查找相关kv信息，并不能做到KV数据库的根据主键直接查询的快速简捷。  

## 2.Chain33中的MVCC机制  
而Chain33当前引入的MVCC机制，借鉴了数据库设计中的MVCC理念（Multi-Version Concurrency Control 多版本并发控制），并结合区块链数据的特点：  
（1）历史数据多（历史区块及交易数据），当前修改的数据少（当前正在生成的区块及交易数据）；  
（2）历史数据不能修改，修改只发生在当前正在生成的区块上；  
（3）读历史数据的比例高（对历史区块数据的读取），写数据的比例低（新生成区块中修改数据）；  
![Store接口时序](./resource/mvcc-show.png)  
根据上图示意：  
主键为Key的变量值在区块高度101、102中均发生变化，对应的在KVDB中有Key:101->value101和Key:102->value102两个历史版本数据，在当前区块104中正在写入新值Key:104->value104；  
那么对于历史数据Key:101、Key:102可以有很多并发的读，对于Key:104有唯一的当前写，这些读写操作可以并发互不影响的执行，并且这些数据可以平铺存储在普通的KVDB中，读写只要根据块高作为版本号与Key构成唯一主键即可（Key:version或者Key:height），不需要构建树形结构来读取历史值和改变当前值，这对于数据读写的效率提升会有比较大的帮助。


# 二、Chain33中的Store及MVCC实现现状 #
## 1.Store与外部的交互消息接口 ##
![Store接口时序](./resource/kvdb-sequence.png)

### EventStoreSet

该消息接口实际未被调用

### EventStoreGet
（1）接口由rpc接口调用时，逐层调用，比如GetAddrOverView/GetBalance/GetTokenBalance等，调用到channelClient的accountdb.LoadAccounts、accountdb.LoadExecAccountQueue等接口时，触发调用QueueProtocol的StoreGet接口，触发向Store发起EventStoreGet消息。  
（2）执行器框架调用StateDB.Get接口时，如果系统配置不支持MVCC，则将通过EventStoreGet消息接口向Store发起数据查询。  
Store在收到EventStoreGet消息时，将根据系统配置调用KVDB或者Mavl的Get接口查询数据，并返回结果。

### EventStoreMemSet
当同步区块或者生成区块时，Chain模块将调用connectBlock->ExecBlock->ExecTx->ExecKVMemSet将执行区块交易的kvset结果通过消息EventStoreMemSet写入Store的缓存。  
Store在收到EventStoreMemSet消息时，将根据系统配置调用KVDB或者Mavl的MemSet接口将中间结果放入缓存，并返回kvset对应的StateHash。

### EventStoreRollback
Chain在ExecBlock的处理中，校验EventStoreMemSet响应消息中的StateHash，如果不一致，则触发ExecKVSetRollback调用，将通过EventStoreRollback消息发起回滚。  
Store收到回滚消息后，会将上一步缓存的内容删除。

### EventStoreCommit
Chain在ExecBlock的处理中，校验EventStoreMemSet响应消息中的StateHash，如果一致，且后续执行正确，触发ExecKVSetCommit调用，将通过EventStoreCommit消息，通知Store持久化对应数据，Store根据系统配置调用KVDB或者Mavl的Commit接口将数据落盘。  

### EventStoreGetTotalCoins
RPC接口调用GetTotalCoins，将通过channelClient调用accountdb.GetTotalCoins接口->调用QueueProtocol的StoreGetTotalCoins接口->通过消息EventStoreGetTotalCoins向Store发起数据查询  
Store目前只有在配置为Mavl时，才支持该消息接口。（Mval实现了IterateRangeByStateHash接口，KVDB未实现该接口）  
## 2.Store相关的类图关系 ##
![Store类图结构](./resource/kvdb-class.png)

## 配置文件中和Store相关的配置
[store]  
name="mavl"  
driver="leveldb"  
dbPath="datadir/kvdb"  
dbCache=128  

## 结合类图关系有如下要点说明：
1.Store根据系统配置决定选择KVStore还是Store（Mavl）。  
2.Store的底层最终使用的db类型根据配置文件来定，可以是GoLevelDB或者GoMemDB，它们都实现的DB interface。  
3.KVStore的实现中，对数据的最新值进行存储，无法保存KV历史值，不能进行版本回滚。  
4.Store（Mval）的实现中，使用merkel avl树形结构存储数据，可以通过statehash索引主键的历史值，但树形结构需要不断的建树、销毁来应对数据变更及查询，当存储数据量多到一定程度后，程序运行速度越来越慢，整体性能受此影响比较大。

## 3.MVCC现状
![mvcc现状](./resource/mvcc.png)  

上图展示了新增区块和删除区块的主要处理逻辑及消息交互。  
### MVCC的控制开关  
[exec]  
enableMVCC=true  
上述字段置为true，则说明系统开启了MVCC能力。  
开启该功能，需要从头开始同步区块，因为系统对于数据的存储格式及存储位置都有变化调整，如果不从头开始同步区块数据，会出问题。  

### 新增区块  
1.ExecBlock的处理中，通过EventExecTxList触发执行器，执行交易（检查交易，处理手续费，调用具体执行器执行交易），获得交易执行结果KV及日志，存储在receipts，并将kv及StateHash写入Store数据库（通过EventStoreMemSet、EventStoreRollback、EventStoreCommit几个消息触发，分为Set-获得stateHash、Rollback-失败回滚、Commit-提交存储几个步骤）。  
2.AddTxs将tx信息（KV）保存到本地数据库，其中要保存的KV数据，经过执行器的处理，如果系统配置为支持MVCC，则对相关KV按MVCC的处理方式进行改造，结果存储本地数据库；也包括手续费信息：TotalFeeKey:hash->fee
  
关于AddMVCC的处理过程描述如下：  
1).首先从本地数据库使用prevHash来获得对应的version  
本地数据库中的对应关系：  
.-mvcc-m.hash->version  
校验版本号信息是对的（hash对应的version与preHash对应的preVersion相差1）  
2).SetVersionKV  
按如下对应的key-value关系，加入kvset  
.-mvcc-m.hash->version  
.-mvcc-m.version.000000000000xx->hash  
3).对于原有的kv进行GetSaveKV处理，生成MVCC的对应version的kv数据，并加入kvset  
.-mvcc-.d.key.version->value  
4).生成对应版本的versionkeylist数据，并加入kvset  
.-mvcc-.m.versionkl.000000000000xx->delkeys
这个数据用于回滚区块时删除数据使用。

### 删除区块  
1.从本地数据中，通过批量删除tx信息  
通过EventDelBlock消息触发执行器执行删除交易，获得要删除的kv的正确格式，如果系统配置为支持MVCC，则相关kv要使用DelMVCC处理改造，才能从本地数据库中正确删除；也包括要删除的手续费信息：  
TotalFeeKey:hash->fee  
  
关于DelMVCC的处理过程描述如下：  
1).获取最大的version,检查与要删除的version相等。（删除区块必须从最高区块开始逆序删除）。  
2).DelVersionKV  
按如下对应的key-value关系，加入要删除的kvset  
.-mvcc-m.hash->version  
.-mvcc-m.version.000000000000xx->hash  
  
新增区块时，保存的对应版本的versionkeylist数据也加入要删除的kvset：
.-mvcc-.m.versionkl.000000000000xx->delkeys  

2.从db中删除block相关的信息

### 开启MVCC与关闭MVCC时的数据读写区别  
![kvdb-mvcc示意图](./resource/mvcc-diff.png)    



# 二、Store模块支持MVCC的KVDB的需求 #
针对Store模块，需要建立一种支持MVCC机制的KVDB实现，达到以下目的：  
1.历史数据可以保留下来，供查询使用，并能够支持数据回滚。  
2.支持MVCC机制，除了保留下来历史版本数据，还可以把多版本数据存储在结构是线性平铺的KV数据库中，不需要使用复杂的树形结构来维护数据，对于系统性能提升有帮助，即使数据量达到一定程度，系统性能不发生明显的下降。

# 三、具体方案描述(正在实现) #

##配置文件中和Store相关的如下配置增加取值
[store]  
name="kvmvcc"  
driver="leveldb"  
dbPath="datadir/kvdb"  
dbCache=128  

当上述配置为kvmvcc时，如下配置项自动失效(值强制设定为false):  
[exec]  
enableMVCC=true  


## 当配置为kvmvcc时，Store将按照如下方式进行statedb数据的维护
![kvdb-mvcc示意图](./resource/kvdb-mvcc.png)  
### 1.增加区块  
Store收到EventStoreMemSet，会将收到的kvset数据打包生成hash(N)（假定新生成的块高为N），并和前一区块的StateHash(N-1)一起生成StateHash(N),即StateHash(N)=hash(StateHash(N-1)+hash(N))。Store将StateHash(N)及对应的version(height)关系数据存入statedb，将增加了MVCC版本号信息的KVSet数据存入statedb，将存入的KVSet的KeyVersionList数据存入statedb(用于在回滚区块时删除数据使用)。

### 2.删除区块（回滚）  
Store需要新增接口EventStoreDel，按照要删除的区块高度height，StateHash信息，删除在statedb中存储的对应version（height）的KVSet数据（先找到KeyVersionList数据，然后根据列表进行删除即可）。  
删除区块的mvcc数据时，遵循倒序删除的规则，比如生成区块数据是0->1->2->3，删除区块数据只能从顶部开始删0->1->2->3->3(删除)->3'->4...  

### 3.查询数据  
当Store收到EventStoreGet消息，可以根据携带的StateHash或者height信息，从statedb中查找相应的key对应的value的值，查询时，key要拼接上version信息（height），以便查找到正确的版本数据。  

### 4.结构层次  
![kvdb-mvcc结构层次示意图](./resource/kvdb-mvcc-arch.png)  
底层为leveldb数据库，提供基础的数据存取能力。  
上层为对外接口，支持Store原来的对外接口，并提供对回滚区块删除数据的新接口EventStoreDel的支持。  
MVCC处理层作为中间层，对传入的kv或者要查询的k，先进行mvcc的加工处理（主要是数据Key的的加工处理：Prefix+Key+version，以便和数据库中的存储数据格式对应起来），再进行数据的存储或者读取，使MVCC的机制能够正常工作。这一层的处理参考现有系统的MVCC实现方式进行改造。  

## 5.与当前实现的差异点分析，参见"开启MVCC与关闭MVCC时的数据读写区别"中的表格描述  
![kvdb-mvcc示意图](./resource/mvcc-diff.png)  