MPT树的基本操作
参考:https://ethfans.org/posts/588
介绍完MPT树的组成结构,在这一章将介绍MPT几种核心的基本操作,也就是我们经常说的 增,删,改,查;为了介绍方便,我们的顺序有变.
一.查(Get)
我们先贴下代码:
func (t *Trie) TryGet(key []byte) ([]byte, error) {
key = keybytesToHex(key) //对原生编码进行Hex编码,这个key其实就是搜索路径
value, newroot, didResolve, err := t.tryGet(t.root, key, 0)//从根节点搜寻与路径内容一致的路径
if err == nil && didResolve {
t.root = newroot
}
return value, err
}
func (t *Trie) tryGet(origNode node, key []byte, pos int) (value []byte, newnode node, didResolve bool, err error) {
switch n := (origNode).(type) {
case nil://如果是个空节点,直接返回空
return nil, nil, false, nil
case valueNode://如果是叶子节点,表示已经找到该节点,直接返回该节点
return n, n, false, nil
case *shortNode://扩展节点
if len(key)-pos < len(n.Key) || !bytes.Equal(n.Key, key[pos:pos+len(n.Key)]) {
// key not found in trie key不是搜索路径的前缀,表示该节点在树中不存在。
return nil, n, false, nil
}
//n.Value为下一个节点的引用,key为前缀,将剩余的搜索路径作为参数,对其子节点递归地调用查找函数
value, newnode, didResolve, err = t.tryGet(n.Val, key, pos+len(n.Key))
if err == nil && didResolve {
n = n.copy()
n.Val = newnode
n.flags.gen = t.cachegen
}
return value, n, didResolve, err
case *fullNode://分支节点
//从Children中找到指向的下一个节点,key为前缀,将剩余的搜索路径作为参数递归地调用查找函数。
value, newnode, didResolve, err = t.tryGet(n.Children[key[pos]], key, pos+1)
if err == nil && didResolve {
n = n.copy()
n.flags.gen = t.cachegen
n.Children[key[pos]] = newnode
}
return value, n, didResolve, err
case hashNode://如果是哈希节点,直接调用t.resolveHash 解析出节点类型,并递归调用查找函数
child, err := t.resolveHash(n, key[:pos])
if err != nil {
return nil, n, true, err
}
value, newnode, _, err := t.tryGet(child, key, pos)
return value, newnode, true, err
default:
panic(fmt.Sprintf("%T: invalid node: %v", origNode, origNode))
}
}
源码部分我加了注释, 为了更形象的表示这个过程。我们通过一幅图进行演示:
上图是查找key为"cat", 节点值为"dog"的过程.
1. keybytesToHex([]byte("cat")),将key"cat"转换成hex编码[3,15,3,13,4,10,T] (在末尾添加终止符是因为需要查找一个真实的数据项内容);
2.t.root的节点类型是shortNode(扩展节点),t.root.Key为3,15,t.root.Val是下一个节点的引用。按代码递归地对其子节点进行查找调用,剩余的搜索路径为[3,13,4,10,T];
3.递归过来的node类型为fullNode(分支节点),以搜索路径[3,13,4,10,T]的第一个字节内容3选择第4个孩子节点递归进行查找,剩余的搜索路径为[13,4,10,T];
4.递归过来的node类型为valueNode(叶子节点),,且key与剩余的搜索路径一致,表示找到了该节点,按代码直接将该节点返回。Val为"dog"。
二.增,改(Insert)
插入操作是基于查找过程完成的,我们也先看代码,再分析过程:
// If a node was not found in the database, a MissingNodeError is returned.
func (t *Trie) TryUpdate(key, value []byte) error {
k := keybytesToHex(key)//对原生编码进行Hex编码,这个key其实就是搜索路径
if len(value) != 0 {
_, n, err := t.insert(t.root, nil, k, valueNode(value))//从树根开始查找插入valueNode
if err != nil {
return err
}
t.root = n
} else {
//`````````略
}
return nil
}
func (t *Trie) insert(n node, prefix, key []byte, value node) (bool, node, error) {
if len(key) == 0 {
//如果key为0,则表示value为一个valueNode,则直接将n转为valueNode并和value进行比较。看看是否是合法的valueNode.
if v, ok := n.(valueNode); ok {
return !bytes.Equal(v, value.(valueNode)), value, nil
}
return true, value, nil
}
switch n := n.(type) {
case *shortNode://扩展节点
matchlen := prefixLen(key, n.Key)//首先找到与当前节点拥有最长相同路径前缀的字节数
if matchlen == len(n.Key) {
//如果和该节点的key完全匹配。表示以前有该并节点。只要更新Val值就行了,dirty表示新的value是否和旧的一样,一样为false.
dirty, nn, err := t.insert(n.Val, append(prefix, key[:matchlen]...), key[matchlen:], value)
if !dirty || err != nil {
return false, n, err
}
//插入成功后,更新shortNode的值然后返回
return true, &shortNode{n.Key, nn, t.newFlag()}, nil
}
//其他情况说明有不同分支,所以构造一个fullNode,然后再fullNode节点的Children位置调用t.insert插入剩下的两个short节点
branch := &fullNode{flags: t.newFlag()}
var err error
_, branch.Children[n.Key[matchlen]], err = t.insert(nil, append(prefix, n.Key[:matchlen+1]...), n.Key[matchlen+1:], n.Val)
if err != nil {
return false, nil, err
}
_, branch.Children[key[matchlen]], err = t.insert(nil, append(prefix, key[:matchlen+1]...), key[matchlen+1:], value)
if err != nil {
return false, nil, err
}
// 如果没有公共前缀部分,则直接返回这个brach节点.
if matchlen == 0 {
return true, branch, nil
}
// 如果有公共前缀,新建公前缀节点,并指向已经插入两个子节点的fullNode,然后return。
return true, &shortNode{key[:matchlen], branch, t.newFlag()}, nil
case *fullNode://分支节点
//直接往对应的孩子节点调用insert方法,然后把新生成的节点更新到fullNode对象的孩子节点
dirty, nn, err := t.insert(n.Children[key[0]], append(prefix, key[0]), key[1:], value)
if !dirty || err != nil {
return false, n, err
}
n = n.copy()
n.flags = t.newFlag()
n.Children[key[0]] = nn
return true, n, nil
case nil://当前是个空树,直接构造一个shortNode.
return true, &shortNode{key, value, t.newFlag()}, nil
case hashNode:
//hashNode的意思是当前节点还没有加载到内存里面来,还是存放在数据库里面,那么首先调用 t.resolveHash(n, prefix)来加载到内存,然后对加载出来的节点调用insert方法来进行插入
rn, err := t.resolveHash(n, prefix)
if err != nil {
return false, nil, err
}
dirty, nn, err := t.insert(rn, prefix, key, value)
if !dirty || err != nil {
return false, rn, err
}
return true, nn, nil
default:
panic(fmt.Sprintf("%T: invalid node: %v", n, n))
}
}
以上代码我也加了注释进行说明,整个过程从根节点开始,一直往下找,直到找到可以插入的点,进行插入操作。参数node是当前插入的节点, prefix是当前已经处理完的部分key, key是还没有处理玩的部分key, 完整的key = prefix + key。 value是需要插入的值。 返回值bool是操作是否改变了Trie树(dirty),node是插入完成后的子树的根节点, error是错误信息
我们继续用一张图来描述下插入过程:
上图是在查找key为"cat"节点的树的基础上插入key为“cau”, value为“dog1”节点的过程。
1.将key"cau"转换成hex编码[3,15,3,13,4,11,T],可以看到和"cat"的hex编码只是最后一位不同。所以“cat"和"cau"的公共前缀是[3,15,3,13,4]这部分。
2.通过查找算法,从树根开始找到左图蓝线圈出的节点node1,且拥有与新插入节点最长的共同前缀[3,15,3,13,4];
3.通过代码可以看到由于(if matchlen == len(n.Key))结果是false,所以就新建了个fullNode,并把"cat","cau"不同的前缀部分[10],[11]为key,分别指向之前val是"dog"和"dog1"的节点.也就是图中的"a"指向"dogt","b"指向"dog1"
4.插入完成。
三.删(Delete)
删除操作其实和插入操作非常相似,不过我们也将代码分析一遍,和上面一样,我也会在代码中加入注释:
func (t *Trie) TryDelete(key []byte) error {
k := keybytesToHex(key) //同上面一样,对原生编码进行Hex编码,这个key其实就是搜索路径
_, n, err := t.delete(t.root, nil, k)//从树根开始查找,删除路径为k的节点.
if err != nil {
return err
}
t.root = n
return nil
}
// delete returns the new root of the trie with key deleted.
// It reduces the trie to minimal form by simplifying
// nodes on the way up after deleting recursively.
func (t *Trie) delete(n node, prefix, key []byte) (bool, node, error) {
switch n := n.(type) {
case *shortNode://扩展节点
matchlen := prefixLen(key, n.Key)//首先找到与当前节点拥有最长相同路径前缀的字节数
if matchlen < len(n.Key) {
return false, n, nil //没有找到相匹配的路径,返回false.
}
if matchlen == len(key) {
return true, nil, nil //找到相匹配的路径了,将整个node删除,返回true.
}
//如果还有路径,以key当前位置递归查找删除剩余的key路径,
dirty, child, err := t.delete(n.Val, append(prefix, key[:len(n.Key)]...), key[len(n.Key):])
if !dirty || err != nil {//没删除成功
return false, n, err
}
switch child := child.(type) {//如果递归删除了个fullNode的子节点
case *shortNode://如果子节点是个shortNode,则将当前节点的key和子节点的key连在一起,组成一个新的shortNode,并更新Val返回.
return true, &shortNode{concat(n.Key, child.Key...), child.Val, t.newFlag()}, nil
default://如果是其他节点,则不用合并key.直接返回
return true, &shortNode{n.Key, child, t.newFlag()}, nil
}
case *fullNode://分支节点
//删除路径中第一个key标志的节点
dirty, nn, err := t.delete(n.Children[key[0]], append(prefix, key[0]), key[1:])
if !dirty || err != nil {
return false, n, err
}
n = n.copy()
n.flags = t.newFlag()//newFlag()表示 dirty标志置为true,hash标志置空.之前的结果已经不可能用
n.Children[key[0]] = nn
//遍历当前fullNode节点还有几个子节点
pos := -1
for i, cld := range &n.Children {
if cld != nil {
if pos == -1 {
pos = i
} else {
pos = -2
break
}
}
}
//
if pos >= 0 {//大于0表示还有子节点,并且pos不等于 16终止符,表示还有子节点
if pos != 16 {
//如果剩余的子节点是个shortNode,则将key合并,生成一个扩展节点返回
cnode, err := t.resolve(n.Children[pos], prefix)
if err != nil {
return false, nil, err
}
if cnode, ok := cnode.(*shortNode); ok {
k := append([]byte{byte(pos)}, cnode.Key...)
return true, &shortNode{k, cnode.Val, t.newFlag()}, nil
}
}
// 否则,直接以当前的key构造一个扩展节点返回,表示已经是叶子节点.
return true, &shortNode{[]byte{byte(pos)}, n.Children[pos], t.newFlag()}, nil
}
// n still contains at least two values and cannot be reduced.
return true, n, nil
case valueNode://叶子节点,直接返回
return true, nil, nil
case nil:
return false, nil, nil
case hashNode:
//hashNode的意思是当前节点还没有加载到内存里面来,还是存放在数据库里面,那么首先调用 t.resolveHash(n, prefix)来加载到内存,然后对加载出来的节点调用t.delete方法来进行删除,同insert
rn, err := t.resolveHash(n, prefix)
if err != nil {
return false, nil, err
}
dirty, nn, err := t.delete(rn, prefix, key)
if !dirty || err != nil {
return false, rn, err
}
return true, nn, nil
default:
panic(fmt.Sprintf("%T: invalid node: %v (%v)", n, n, key))
}
}
代码看完。我们总结下规则:
1. 找到与需要插入的节点拥有最长相同路径前缀的节点,记为Node;
2. 若Node为叶子/扩展节点(shortNode):"若剩余的搜索路径与node的Key完全一致,则将整个node删除;若node的key是剩余搜索路径的前缀,则对该节点的Val做递归的删除调用,否则删除失败".
3.若Node为分支节点: 删除孩子列表中相应下标标志的节点;删除结束,若Node的孩子个数只剩下一个,那么将分支节点替换成一个叶子/扩展节点(shortNode);
4.若删除成功,则将被修改节点的dirty标志置为true,hash标志置空(之前的结果已经不可能用),且将节点的诞生标记更新为现在;
删除过程我们也有图,来描述一下:
这张图就是删除上面插入的节点(key为“cau”, value为“dog1”)。
1.将key"cau"转换成hex编码[3,15,3,13,4,11,T] ;
2.通过查找算法,找到用叉(X)号表示的节点node1,从根节点到node1的路径与搜索路径完全一致
3.从node1的父节点中删除该节点,父节点仅剩一个孩子节点,故将父节点转换成一个叶子节点;
4.新生成的叶子节点又与其父节点(扩展节点)发生了合并,最终生成了一个叶子节点包含了所有的信息.
最后的树又恢复到了最开始的时候。
OK。关于树的操作。基本上就是这些,不过还有一个非常重要的就是树的存储。我们下篇再介绍.