【MyDB】5-索引管理之 3-索引实现之Node

[!tip]

代码位于top/xianghua/mydb/server/im/Node.java中

前言

刚刚说过,我们在**Node 封装单个节点的数据结构与本地操作(**插入、分裂、查询)

Node节点结构设计

Node 类的核心是 紧凑的字节级内存布局,直接操作原始字节数组以实现高效存储。以下是节点的二进制结构:

// 节点内存布局(单位:字节)
[LeafFlag(1)][KeyNumber(2)][SiblingUid(8)][Son0(8)][Key0(8)]...[SonN(8)][KeyN(8)]
字段字节数描述
LeafFlag1是否为叶子节点(1: 是,0: 否)
KeyNumber2当前节点存储的键值对数量(最大为 2*BALANCE_NUMBER
SiblingUid8兄弟节点的 UID(仅叶子节点有效,形成链表结构)
SonX/KeyX8+8子节点 UID 和对应的键值对(非叶子节点)或数据 UID 和键(叶子节点)

具体实现

常量定义

在Node.java中,先定义了以下基础常量

静态常量描述
IS_LEAF_OFFSET0标识节点是否为叶子节点(1字节)
NO_KEYS_OFFSET1存储节点当前键值对数量(2字节,最大 65535)
SIBLING_OFFSET3兄弟节点 UID(8字节,仅叶子节点有效)
NODE_HEADER_SIZE11节点头部总大小(1+2+8=11字节)
BALANCE_NUMBER32平衡因子,决定节点分裂阈值(键数达到 64 时分裂)
NODE_SIZE1075节点总大小(11 + (8+8)(322+2) = 11 + 16*66 = 1075 字节)
    static final int IS_LEAF_OFFSET = 0;
    static final int NO_KEYS_OFFSET = IS_LEAF_OFFSET+1;
    static final int SIBLING_OFFSET = NO_KEYS_OFFSET+2;
    static final int NODE_HEADER_SIZE = SIBLING_OFFSET+8;

    static final int BALANCE_NUMBER = 32;
    static final int NODE_SIZE = NODE_HEADER_SIZE + (2*8)*(BALANCE_NUMBER*2+2);

    BPlusTree tree;
    DataItem dataItem; // 指向dataItem的data()
    SubArray raw; // 指向dataItem的data()
    long uid;

对节点中内容读取和操作的方法

Node.java中是通过字节来存储Node节点的数值的,因此获取Node的相关属性不能够直接通过node.isLeaf来获得,而是要计算在共享数组中的偏移量来获得。

因此提供以下方法来读取/设置Node节点的值

设置是叶子节点/读取是否为叶子节点:setRawIsLeaf/getRawIfLeaf

设置节点键个数/读取节点键个数 setRawNoKeys/getRawNoKeys

设置节点兄弟节点uid/读取节点兄弟节点uid setRawSibling/getRawSibling

设置第k个key的uid / 读取第k个key的uid setRawKthSon/getRawKthSon

设置第k个key/读取第k个key setRawKthKey/getRawKthKey

    static void setRawIsLeaf(SubArray raw, boolean isLeaf) {
        if(isLeaf) {
            raw.raw[raw.start + IS_LEAF_OFFSET] = (byte)1;
        } else {
            raw.raw[raw.start + IS_LEAF_OFFSET] = (byte)0;
        }
    }

    static boolean getRawIfLeaf(SubArray raw) {
        return raw.raw[raw.start + IS_LEAF_OFFSET] == (byte)1;
    }

    static void setRawNoKeys(SubArray raw, int noKeys) {
        System.arraycopy(Parser.short2Byte((short)noKeys), 0, raw.raw, raw.start+NO_KEYS_OFFSET, 2);
    }

    static int getRawNoKeys(SubArray raw) {
        return (int)Parser.parseShort(Arrays.copyOfRange(raw.raw, raw.start+NO_KEYS_OFFSET, raw.start+NO_KEYS_OFFSET+2));
    }

    static void setRawSibling(SubArray raw, long sibling) {
        System.arraycopy(Parser.long2Byte(sibling), 0, raw.raw, raw.start+SIBLING_OFFSET, 8);
    }

    static long getRawSibling(SubArray raw) {
        return Parser.parseLong(Arrays.copyOfRange(raw.raw, raw.start+SIBLING_OFFSET, raw.start+SIBLING_OFFSET+8));
    }

    static void setRawKthSon(SubArray raw, long uid, int kth) {
        int offset = raw.start+NODE_HEADER_SIZE+kth*(8*2);
        System.arraycopy(Parser.long2Byte(uid), 0, raw.raw, offset, 8);
    }

    static long getRawKthSon(SubArray raw, int kth) {
        int offset = raw.start+NODE_HEADER_SIZE+kth*(8*2);
        return Parser.parseLong(Arrays.copyOfRange(raw.raw, offset, offset+8));
    }

    static void setRawKthKey(SubArray raw, long key, int kth) {
        int offset = raw.start+NODE_HEADER_SIZE+kth*(8*2)+8;
        System.arraycopy(Parser.long2Byte(key), 0, raw.raw, offset, 8);
    }

    // 从subArray数组中获取第k-th个键值
    static long getRawKthKey(SubArray raw, int kth) {
        int offset = raw.start+NODE_HEADER_SIZE+kth*(8*2)+8; // (8*2)每个键值对占用的字节数 +8(因为键值对中键的偏移量是在值的偏移量之后)
        // raw中复制出从offset开始的8个字节解析为一个long类型的值
        return Parser.parseLong(Arrays.copyOfRange(raw.raw, offset, offset+8));
    }

    }

其他字节操作

    static void copyRawFromKth(SubArray from, SubArray to, int kth) {
        int offset = from.start+NODE_HEADER_SIZE+kth*(8*2);
        System.arraycopy(from.raw, offset, to.raw, to.start+NODE_HEADER_SIZE, from.end-offset);
    }

    static void shiftRawKth(SubArray raw, int kth) {
        int begin = raw.start+NODE_HEADER_SIZE+(kth+1)*(8*2);
        int end = raw.start+NODE_SIZE-1;
        for(int i = end; i >= begin; i --) {
            raw.raw[i] = raw.raw[i-(8*2)];
        }

节点创建与加载方法

  • 创建新节点

    newRootRaw该根节点的初始两个子节点为 left 和 right, 初始键值为 key。

    static byte[] newRootRaw(long left, long right, long key) // 新建根节点
    static byte[] newNilRootRaw()                              // 新建空叶子节点
    
    • 示例:调用 newRootRaw(100, 200, 50) 生成根节点,包含两个子节点(UID=100 和 200),分裂键为 50。

    • 完整代码

         /**
           * 根据传入的左右节点的uid和当前节点的key,创建一个新的根节点
           * @param left
           * @param right
           * @param key
           * @return
           */
          static byte[] newRootRaw(long left, long right, long key)  {
              SubArray raw = new SubArray(new byte[NODE_SIZE], 0, NODE_SIZE);
      
              setRawIsLeaf(raw, false);
              setRawNoKeys(raw, 2);  // 设置节点的键的数量为2
              setRawSibling(raw, 0); // 设置节点的兄弟节点的UID为0
              setRawKthSon(raw, left, 0); // 设置第0个子节点的UID为left
              setRawKthKey(raw, key, 0);   // 设置第0个键的值为key
              setRawKthSon(raw, right, 1);  // 设置第1个子节点的UID为right
              setRawKthKey(raw, Long.MAX_VALUE, 1); // 最后一个key
      
              return raw.raw;
          }
      
          /**
           * 创建一个空的叶子节点
           * @return
           */
          static byte[] newNilRootRaw()  {
              SubArray raw = new SubArray(new byte[NODE_SIZE], 0, NODE_SIZE);
      
              setRawIsLeaf(raw, true);
              setRawNoKeys(raw, 0); // 没有key
              setRawSibling(raw, 0); // 没有兄弟节点
      
              return raw.raw;
          }
      
  • 加载节点

    static Node loadNode(BPlusTree bTree, long uid) // 从 DataManager 加载节点
    
    • 流程:根据 UID 读取 DataItem,解析字节数组生成 Node 对象。

    • 完整代码

          static Node loadNode(BPlusTree bTree, long uid) throws Exception {
              DataItem di = bTree.dm.read(uid);
              assert di != null;
              Node n = new Node();
              n.tree = bTree;
              n.dataItem = di;
              n.raw = di.data();
              n.uid = uid;
              return n;
          }
      
          public void release() {
              dataItem.release();
          }
      

查询

Node 类有两个方法,用于辅助 B+ 树做插入和搜索操作,分别是 searchNext 方法和 leafSearchRange 方法。

searchNext 寻找对应 key 的 UID, 如果找不到, 则返回兄弟节点的 UID。

  • 单键搜索 (searchNext)

    public SearchNextRes searchNext(long key)
    
    • 逻辑:遍历节点的键,找到第一个大于 key 的子节点 UID;若未找到,返回兄弟节点 UID。

    • 示例:节点键为 [20, 40, 60],搜索 key=30 返回 UID 对应键 40 的子节点。

    • 具体代码

      • 存储搜索到的结果SearchNextRes

        
            // 存储搜索到的结果
            class SearchNextRes {
                long uid;
                long siblingUid;
            }
        
      • searchNext,找到第一个大于 key 的子节点 UID

           /**
             * 在B+树中查找大于给定key的下一个节点
             * @param key
             * @return
             */
            public SearchNextRes searchNext(long key) {
                // 1.获取读锁
                dataItem.rLock();
                try {
                    SearchNextRes res = new SearchNextRes();
                    int noKeys = getRawNoKeys(raw); // 获取key的数量
                    // 2.遍历key,找到第一个大于key的节点
                    for(int i = 0; i < noKeys; i ++) {
                        // 2.1 获取第i个key
                        long ik = getRawKthKey(raw, i);
                        // 2.2 如果key小于ik,则返回第ik个节点
                        if(key < ik) {
                            res.uid = getRawKthSon(raw, i);
                            res.siblingUid = 0;
                            return res;
                        }
                    }
                    // 3.如果遍历完所有的key都没有找到大于key的节点,则返回兄弟节点
                    // 3.1 设置uid为0,表示没有找到
                    res.uid = 0;
                    // 3.2 设置siblingUid为当前节点的兄弟节点
                    res.siblingUid = getRawSibling(raw);
                    return res;
        
                } finally {
                    dataItem.rUnLock();
                }
            }
        
  • 范围查询 (leafSearchRange)

    leafSearchRange 方法在当前节点进行范围查找,范围是 [leftKey, rightKey],这里约定如果 rightKey 大于等于该节点的最大的 key, 则还同时返回兄弟节点的 UID,方便继续搜索下一个节点。

    public LeafSearchRangeRes leafSearchRange(long leftKey, long rightKey)
    
    • 流程

      1. 定位第一个 >= leftKey 的键。
      2. 收集所有 <= rightKey 的 UID。
      3. 若遍历完所有键,通过 siblingUid 跳转到下一叶子节点。
    • 结果存储 uids存储找到的左右节点uid,如果 rightKey 大于等于该节点的最大的 key, 则返回兄弟节点的 UID,siblingUid,方便继续搜索下一个节点。

          class LeafSearchRangeRes {
              List<Long> uids;
              long siblingUid;
          }
      
    • leafSearchRange,在B+树的叶子节点中搜索一个键值范围

      
          /**
           * 在B+树的叶子节点中搜索一个键值范围的方法
           */
          public LeafSearchRangeRes leafSearchRange(long leftKey, long rightKey) {
              dataItem.rLock();
              try {
                  int noKeys = getRawNoKeys(raw); // 获取key的数量
                  int kth = 0;
                  // 2.遍历key,找到第一个大于leftKey的节点
                  while(kth < noKeys) {
                      long ik = getRawKthKey(raw, kth);
                      if(ik >= leftKey) {
                          break;
                      }
                      kth ++;
                  }
                  // 3.循环将所有小于等于rightKey对应的子节点的uid添加到uids列表中
                  List<Long> uids = new ArrayList<>();
                  while(kth < noKeys) {
                      long ik = getRawKthKey(raw, kth);
                      if(ik <= rightKey) {
                          uids.add(getRawKthSon(raw, kth));
                          kth ++;
                      } else {
                          break;
                      }
                  }
                  // 4.返回uids列表和siblingUid
                  long siblingUid = 0; //
                  if(kth == noKeys) { // 如果遍历完了所有的key,则返回兄弟节点
                      siblingUid = getRawSibling(raw);
                  }
                  LeafSearchRangeRes res = new LeafSearchRangeRes();
                  res.uids = uids;
                  res.siblingUid = siblingUid;
                  return res;
              } finally {
                  dataItem.rUnLock();
              }
          }
      

插入与分裂

  • 插入键值对 (insert)

    private boolean insert(long uid, long key)
    
    • 叶子节点插入

      • 移动后续键值对腾出位置,插入新键和 UID。
      • 示例:原键 [10, 30, 50],插入 key=20 后变为 [10, 20, 30, 50](需分裂判断)。
    • 非叶子节点插入

      • 更新分裂键,插入新子节点 UID 和键(如更新中间节点的路由键)。
  • 节点分裂 (split)

    private SplitRes split() throws Exception
    
    • 触发条件:键数达到 2 * BALANCE_NUMBER(默认 64)。
    • 步骤
      1. 创建新节点,复制原节点后半部分键值。
      2. 原节点保留前 BALANCE_NUMBER 个键。
      3. 更新原节点的 siblingUid 指向新节点。
      4. 将新节点插入 DataManager,返回其首键和 UID。

插入

先定义一个InsertAndSplitRes类存储返回结果

    class InsertAndSplitRes {
        long siblingUid, newSon, newKey;
    }

insertAndSplit方法,调用insert方法直接插入uid和key,插入后会根据当前节点数量判断是否需要分裂

    public InsertAndSplitRes insertAndSplit(long uid, long key) throws Exception {
        boolean success = false; // 标志位,用于判断是否插入成功
        Exception err = null; // 用于存储异常信息
        InsertAndSplitRes res = new InsertAndSplitRes(); // 存储插入结果

        dataItem.before(); // 数据项上设置一个保存点
        try {
            success = insert(uid, key); // 调用insert方法插入键值对,返回插入结果
            if(!success) { // 如果插入失败,则设置兄弟节点的uid,并返回结果
                res.siblingUid = getRawSibling(raw);
                return res;
            }
            if(needSplit()) { // 如果需要分裂,则调用split方法进行分裂
                try {
                    SplitRes r = split(); // 调用split方法进行分裂,返回分裂结果
                    res.newSon = r.newSon; // 设置新节点的uid
                    res.newKey = r.newKey; // 设置新节点的键值
                    return res; // 返回分裂结果
                } catch(Exception e) {
                    err = e;
                    throw e;
                }
            } else {
                return res; // 不需要分裂则直接返回结果
            }
        } finally {
            if(err == null && success) {
                // 没有发生错误并且插入成功,提交数据项的修改
                dataItem.after(TransactionManagerImpl.SUPER_XID);
            } else {
                // 发生错误或者插入失败,回滚数据项的修改
                dataItem.unBefore();
            }
        }
    }

insert方法

插入方法包含两种情况

  • 叶子节点:找到该节点应该在的位置(k),后移k后面的所有节点

  • 非叶子节点:找到该节点应该在的位置k,保存位置k的key为kk,更新位置k的key为要插入的key

    之后后移k+1的所有节点

    将k+1位置设置成原来的位置k的key,kk

    k+1位置的uid设置成当前的uid

    感觉这里有点莫名其妙,我暂时还没有看懂为什么要这样设计

    /**
     * 向B+树中插入一个键值对
     * @param uid
     * @param key
     * @return
     */
    private boolean insert(long uid, long key) {
        // 1.获取节点中key的数量
        int noKeys = getRawNoKeys(raw);
        int kth = 0;
        // 2.遍历key,找到第一个大于key的节点
        while(kth < noKeys) {
            long ik = getRawKthKey(raw, kth);
            if(ik < key) {
                kth ++;
            } else {
                break;
            }
        }
        // 3.如果kth等于key的数量,并且兄弟节点不为空,则插入失败,返回false
        if(kth == noKeys && getRawSibling(raw) != 0) return false;

        // 4.如果是叶子节点
        if(getRawIfLeaf(raw)) {
            shiftRawKth(raw, kth); // 将kth之后的key和子节点向后移动一位
            // 在插入位置插入新的key和子节点uid
            setRawKthKey(raw, key, kth);
            setRawKthSon(raw, uid, kth);
            setRawNoKeys(raw, noKeys+1); // 更新key的数量
        } else {
            // 5.如果不是叶子节点
            // 5.1 获取插入位置的key
            long kk = getRawKthKey(raw, kth);
            setRawKthKey(raw, key, kth); // 更新插入位置的key
            shiftRawKth(raw, kth+1); // 将kth+1之后的key和子节点向后移动一位
            setRawKthKey(raw, kk, kth+1);
            setRawKthSon(raw, uid, kth+1);
            setRawNoKeys(raw, noKeys+1);
        }
        return true;
    }

分裂

判断是否需要分裂

private boolean needSplit() {
    return BALANCE_NUMBER*2 == getRawNoKeys(raw);
}



存储分裂结果

class SplitRes {
    long newSon, newKey;
}

分裂逻辑


    /**
     * 分裂B+树节点的方法
     * 可以理解为将一个节点分裂成两个节点
     * 举例:node = [10,40,50,60] -> [10,40] [50,60]
     * @return
     * @throws Exception
     */
    private SplitRes split() throws Exception {
        // 1.创建一个新的节点
        SubArray nodeRaw = new SubArray(new byte[NODE_SIZE], 0, NODE_SIZE);

        // 2.设置新节点的属性
        setRawIsLeaf(nodeRaw, getRawIfLeaf(raw)); //设置新节点的叶子节点属性,和当前节点相同
        setRawNoKeys(nodeRaw, BALANCE_NUMBER); // 设置新节点的key数量为平衡因子,即原节点键数的一半
        setRawSibling(nodeRaw, getRawSibling(raw));// 设置新节点的兄弟节点为原节点的兄弟节点

        //3. 将原节点的后半部分复制到新节点
        copyRawFromKth(raw, nodeRaw, BALANCE_NUMBER);
        // 4.将节点插入到DM中,并返回新节点的uid
        long son = tree.dm.insert(TransactionManagerImpl.SUPER_XID, nodeRaw.raw);

        // 5.更新原节点的属性
        // 5.1 设置原节点的key数量为平衡因子
        setRawNoKeys(raw, BALANCE_NUMBER);
        // 5.2 设置原节点的兄弟节点为新节点的uid
        setRawSibling(raw, son);

        // 6.返回新节点的uid和key
        SplitRes res = new SplitRes();
        res.newSon = son;
        res.newKey = getRawKthKey(nodeRaw, 0);
        return res;
    }

参考资料

索引管理 | EasyDB (blockcloth.cn)

MYDB 8. 索引管理 | 信也のブログ (shinya.click)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值