leveldb memdb源码分析(下)之Rust实现篇

前言

leveldb中memdb模块使用skiplist作为一个kv的内存存储,相关代码实现非常漂亮。在上文介绍了下面内容:

  • 对比c++和golang版本中查询、插入、删除的实现
  • 分析golang版本中可以优化的地方,然后对rust版本进行优化

然后在本文中将会介绍

  • 如何参考goleveldb的版本使用rust重写memdb(arena版本)
  • 使用rust重写一个非arena版本的memdb,也就是经典的链表结构实现方式

arena实现

参考 goleveldb DB 的代码,同时考虑到并发安全,所以在Rust实现中,分别定义了dbDb 两个结构

  • db包含所有的成员,非线程安全,提供查询相关方法
  • Dbdb添加Mutex封装,线程安全,提供核心的插入,删除功能以及更多的查询功能

具体如下:

db

github db

struct db<T: Comparer> {
   
    cmp: T,  // 比较器,用于比较key
    // rnd:rand
    // 存储实际key,value的数据
    kv_data: Vec<u8>,
    // 用于记录key,value在kv_data中的索引 ,每一个节点的格式如下 ,其中 level 表示当前节点的层数,后跟 level 个数字,分别表示当前节点的level个层中每一层的下一个节点在node_data中的位置
    // kvOffset1|len(key1)|len(value1)|level|next_node_1|...|next_node_i|...|next_node_h|
    node_data: Vec<usize>, // 前面16个字节对应于 head 节点
    // 在查询过程中,记录搜索过程中所经历的节点(实际是节点在node_data的位置),主要用于插入和删除
    prev_node: RefCell<[usize; MAX_HEIGHT]>,
    // 当前skiplist的层数
    max_height: usize,
    // skiplist中的节点个数
    n: usize,
    // 数据总大小
    kv_size: usize,
}

这个db定义和goleveldb 定义的是非常类似的,没有太多复杂的地方。

不过需要注意的是这里db中的prev_node成员变量,用于在查询或删除过程中记录每一层的前向节点,在golveldb中是一个普通的数组prevNode ,在我们的Rust定义中是一个用RefCell封装的数组 RefCell<[usize; MAX_HEIGHT]> ,原因在于db的搜索方法有两种使用场景,一种是用于纯粹的搜索查询,那么当前db就是只读的,使用不变借用 &self,如果用于插入或删除,需要往RefCell中插入数据,那么db就变成可变了,需要使用可变借用 &mut self,为了让db保持 不变借用语义,所以使用RefCell来提供内部可变特性。那么为什么要让db保持不变借用,直接不管 纯查询或修改查询都使用可变借用不就行了吗?

因为Rust中不变借用是可以共享的,而可变借用是不可以共享的,如果直接只用可变借用&mut self的话,就会限制纯 查询操作的使用场景,即使一个操作只是查询,也要将db声明为mut。

另外一种方法就是prev_node 不作为db的成员变量,而是在查询的时候作为一个额外的函数入参,具体可以参考 节点版本的做法。

封装next节点的读取和设置

github 地址

为了提高代码的可读性和可维护性,将 获取node节点在level层的下一个节点在node_data中的位置的操作和 设置下一个节点的操作进行封装:

// 计算node节点在level层的下一个节点在node_data中的位置 ,封装一下,提高代码可读性
    fn next_node(&self, node: usize, i: usize) -> usize {
   
        // + NNEXT 表示在node_data 中,从node位置开始,要跳过  kvOffset1|len(key1)|len(value1)|level| 这4个字节,后面再移动 i 个位置,就到达 next_node_i 了
        self.node_data[node + NNEXT + i]
    }

    fn set_next_node(&mut self, node: usize, i: usize, next: usize) {
   
        self.node_data[node + NNEXT + i] = next;
    }

查询大于等于特定key的节点

github 地址

可以看到经过封装,find_great_or_equal的实现方式与skiplist的算法描述更加贴合。

// save_pre 标记 在搜索过程中是否要记录遍历过的节点
    pub fn find_great_or_equal(&self, key: &internalKey, save_pre: bool) -> (usize, bool) {
   
        let mut node = 0;
        // 从高层到底层开始搜索
        let mut i = self.max_height - 1;
        // println!("max_height {}", i);

        loop {
   
            // 下个节点在 node_data 中的位置
            let next = self.next_node(node, i);
            let mut cmp = Ordering::Greater;
            // 当前链表上没有走到尾
            if next > 0 {
   
                // 和下个节点next进行key比较
                cmp = self.cmp.compare(self.get_key_data(next), key.data());
            }

            // 大于下一个节点,继续沿着当前层 向右 跳
            if cmp == Ordering::Less {
   
                node = next;
            } else {
   
                // 小于等于下一个节点 或 下一个节点是空
                // if save_pre {
   
                //     // 对于插入或删除而进行的搜索,即使遇到相同的也要继续往下一层比较,不能立即返回
                //     // 所以这里要先于 cmp == Ordering::Equal 的判断
                //     self.prev_node.borrow_mut()[i] = node;
                // } else if cmp == Ordering::Equal {
   
                //     // find_great_or_equal 跟 find_less 的一个不同就是这里返回的是 next
                //     return (next, true);
                // }

                // 改成下面的方式可读性更高
                if (!save_pre) && cmp == Ordering::Equal {
   
                    return (next, true);
                }
                if save_pre {
   // 如果需要保持前向节点,记录到pre_node中
                    self.prev_node.borrow_mut()[i] = node;
                }

                if i == 0 {
   
                    return (next, cmp == Ordering::Equal);
                }

                i -= 1;
            }
        }
    }

在上面实现中,对于当前节点小于等于下一个节点的处理,相比golang的写法进行了重构。参考golang的写法如下:

// 小于等于下一个节点 或 下一个节点是空
                if save_pre {
   
                     // 对于插入或删除而进行的搜索,即使遇到相同的也要继续往下一层比较,不能立即返回
                     // 所以这里要先于 cmp == Ordering::Equal 的判断
                     self.prev_node.borrow_mut()[i] = node;
                 } else if cmp == Ordering::Equal {
   
                     // find_great_or_equal 跟 find_less 的一个不同就是这里返回的是 next
                     return (next, true);
                 }

这里代码的主要含义是:如果只是纯粹的查询操作的话,找到匹配的就可以直接返回了;但是如果是为了插入或删除而进行的查询,即使找到了匹配的节点也要往下一层跳,直到最下面的一层才可以返回。理解了代码的意思我们进行重写

// 改成下面的方式可读性更高
                if (!save_pre) && cmp == Ordering::Equal {
   
                    return (next, true);
                }
                if save_pre {
   
                    self.prev_node.borrow_mut()[i] = node;
                }

其它

find_lessthan, find_last 这两个method的Rust实现跟goleveldb一致,就不多讲,大家可以直接点击去看源码。

Db

github Db

pub struct Db<T: Comparer> {
   
    db: sync::RwLock<db<T>>,
}

db主要用来提供搜索方法,非线程安全的, Db执行插入和删除,线程安全。

插入put

github 插入

首先获取写锁,在Rust中,sync::RwLock通过调用write() 方法获取写锁:

let mut db = self.db.write

你可能感兴趣的:(DEEPNOVA开发者社区,rust,开发语言,后端)