手写数据库轮子项目 MYDB 之八 | IndexManager (IM) 索引

IM,即 Index Manager,索引管理器,为 MYDB 提供了基于 B+ 树的非聚簇索引。索引的数据被直接插入数据库文件中,而不需要经过版本管理。

一、Node 节点

二叉树由一个个 Node 组成,每个 Node 都存储在一条 DataItem 中的 [Data] 部分。结构如下:

[LeafFlag][KeyNumber][SiblingUid]
  1字节      2字节       8字节
[Son0][Key0][Son1][Key1]...[SonN][KeyN]
8字节  8字节 8字节...

其中 LeafFlag 标记了该节点是否是个叶子节点,KeyNumber 为该节点中 key 的个数,SiblingUid 是其兄弟节点存储在 DM 中的 UID。后续是穿插的子节点 SonN(索引的 uid) 和 KeyN (索引)。最后的一个 KeyN 始终为 MAX_VALUE,以此方便查找。

Node 类持有了其 B+ 树结构的引用,DataItem 的引用和 SubArray 的引用,用于方便快速修改数据和释放数据。

public class Node {
    BPlusTree tree;
    DataItem dataItem;
    SubArray raw;
    long uid;
    ...
}

1. 生成根节点

生成一个根节点的数据可以写成如下:

    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); /// 设置 key 的数量为2,有两个子节点
        setRawSibling(raw, 0); /// 设置兄弟节点的 uid,根节点无邻节点
        setRawKthSon(raw, left, 0); /// 设置第0个子节点的 uid 为 left
        setRawKthKey(raw, key, 0); /// 设置第0个子节点的 key 为 key
        setRawKthSon(raw, right, 1); /// 设置第1个子节点的 uid 为 right
        setRawKthKey(raw, Long.MAX_VALUE, 1); /// 设置第1个子节点的 key 为 MAX_VALUE

        return raw.raw;
    }

类似,生成一个空的根节点:

    static byte[] newNilRootRaw()  {
        SubArray raw = new SubArray(new byte[NODE_SIZE], 0, NODE_SIZE);

        setRawIsLeaf(raw, true);
        setRawNoKeys(raw, 0);
        setRawSibling(raw, 0);

        return raw.raw;
    }

2. 搜索

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

定义一个搜索到的节点:

    class SearchNextRes {
        long uid;
        long siblingUid;
    }

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

    public SearchNextRes searchNext(long key) {
        dataItem.rLock();
        try {
            SearchNextRes res = new SearchNextRes();
            int noKeys = getRawNoKeys(raw); /// 该节点的 key 的数量
            for(int i = 0; i < noKeys; i ++) {
                long ik = getRawKthKey(raw, i); /// 返回 key
                if(key < ik) { /// 找到了,返回 key 的 uid
                    res.uid = getRawKthSon(raw, i);
                    res.siblingUid = 0;
                    return res;
                }
            }
            /// 找不到,返回兄弟节点的 uid
            res.uid = 0;
            res.siblingUid = getRawSibling(raw);
            return res;

        } finally {
            dataItem.rUnLock();
        }
    }

定义多个搜索到的节点:

    class LeafSearchRangeRes {
        List<Long> uids;
        long siblingUid;
    }

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

3. 插入

如果要插入的是叶子节点,kth之后的节点右移,把key插入到kth位置。

如果要插入的不是叶子节点,kth+1之后的节点右移,新插入节点的uid和原kth位置的key放到kth+1上,原kth位置的uid和待插入的key放到原kth位置。

    private boolean insert(long uid, long key) {
        int noKeys = getRawNoKeys(raw);
        int kth = 0;
        while(kth < noKeys) {
            long ik = getRawKthKey(raw, kth);
            if(ik < key) {
                kth ++;
            } else {
                break;
            }
        }
        /// 应该插入到 raw 的 kth 位置
        /// 要插入节点要插在当前节点的最后位置,且当前节点已经有邻节点,返回插入失败,下一步在邻节点进行插入
        if(kth == noKeys && getRawSibling(raw) != 0) return false;
        /// 如果当前节点是叶子节点
        if(getRawIfLeaf(raw)) {
            /// 从 kth 开始(包括 kth)所有节点往右移动,新的节点插入到kth位置
            shiftRawKth(raw, kth);
            setRawKthKey(raw, key, kth);
            setRawKthSon(raw, uid, kth);
            setRawNoKeys(raw, noKeys+1);
        } else { /// 如果当前节点不是叶子节点
            long kk = getRawKthKey(raw, kth);
            setRawKthKey(raw, key, kth); /// kth 位置放入新插入的 key
            shiftRawKth(raw, kth+1); /// 从 kth + 1 开始所有节点往右移动
            setRawKthKey(raw, kk, kth+1); /// 原 kth 上的 key 放到 kth + 1 上
            setRawKthSon(raw, uid, kth+1); /// 新插入节点的 uid 放到 kth + 1 上
            setRawNoKeys(raw, noKeys+1);
        }
        return true;
    }

4. 分裂

每当插入节点后都要判断是否需要分裂。

    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); /// 插入是否成功
            if(!success) { /// 失败,返回兄弟节点
                res.siblingUid = getRawSibling(raw);
                return res;
            }
            if(needSplit()) { /// 是否需要分裂
                try { /// 新分裂出来的节点的 uid 和 key 赋值给 newSon 和 newKey
                    SplitRes r = split();
                    res.newSon = r.newSon;
                    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();
            }
        }
    }

当子节点装满时,需要分裂出兄弟节点。

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

分裂流程如下。 

    private SplitRes split() throws Exception {
        SubArray nodeRaw = new SubArray(new byte[NODE_SIZE], 0, NODE_SIZE); /// 创建新节点
        setRawIsLeaf(nodeRaw, getRawIfLeaf(raw)); ///设置叶子节点标志位
        setRawNoKeys(nodeRaw, BALANCE_NUMBER); /// 设置新节点 key 的数量为 BALANCE_NUMBER
        setRawSibling(nodeRaw, getRawSibling(raw)); /// 设置兄弟节点为原节点的兄弟节点
        copyRawFromKth(raw, nodeRaw, BALANCE_NUMBER); /// 把原节点后一半的数据拷贝到新节点
        long son = tree.dm.insert(TransactionManagerImpl.SUPER_XID, nodeRaw.raw); /// 得到新节点的 uid
        setRawNoKeys(raw, BALANCE_NUMBER); /// 设置原节点 key 的数量为 BALANCE_NUMBER
        setRawSibling(raw, son); /// 设置原节点的兄弟节点为新节点

        SplitRes res = new SplitRes();
        res.newSon = son;  //返回兄弟节点的 uid 和 第一个 key
        res.newKey = getRawKthKey(nodeRaw, 0);
        return res;
    }

二、B+ 树

1. 根节点

由于 B+ 树在插入删除时,会动态调整,根节点不是固定节点,于是设置一个 bootDataItem,该 DataItem 中存储了根节点的 UID。

    private long rootUid() {
        bootLock.lock();
        try {
            SubArray sa = bootDataItem.data();
            return Parser.parseLong(Arrays.copyOfRange(sa.raw, sa.start, sa.start+8));
        } finally {
            bootLock.unlock();
        }
    }

动态调整根节点如下,可以注意到,IM 在操作 DM 时,使用的事务都是 SUPER_XID,表示永远处于可提交状态。 

    private void updateRootUid(long left, long right, long rightKey) throws Exception {
        bootLock.lock();
        try {
            byte[] rootRaw = Node.newRootRaw(left, right, rightKey); /// 生成根节点
            long newRootUid = dm.insert(TransactionManagerImpl.SUPER_XID, rootRaw); /// 在数据页中插入根节点并返回 uid
            bootDataItem.before();
            SubArray diRaw = bootDataItem.data();
            /// 替换 uid 为新的 uid
            System.arraycopy(Parser.long2Byte(newRootUid), 0, diRaw.raw, diRaw.start, 8);
            bootDataItem.after(TransactionManagerImpl.SUPER_XID);
        } finally {
            bootLock.unlock();
        }
    }

2. 节点插入

在一个完整的B+树节点插入时,需要从根节点搜索,直到寻找需要插入的叶子节点位置进行插入。如果根节点已满,需要生成一个新的根节点,动态调整根节点信息。

    public void insert(long key, long uid) throws Exception {
        /// rootUid 节点(一直在变,但永远是 bootItem 里的值)
        long rootUid = rootUid();
        /// 从根节点开始,找到要插入的叶子节点
        InsertRes res = insert(rootUid, uid, key);
        assert res != null;
        if(res.newNode != 0) {
            /// 根节点已满,需要分裂新根节点
            updateRootUid(rootUid, res.newNode, res.newKey);
        }

    private InsertRes insert(long nodeUid, long uid, long key) throws Exception {
        Node node = Node.loadNode(this, nodeUid);
        boolean isLeaf = node.isLeaf();
        node.release();

        InsertRes res = null;
        if(isLeaf) { /// 如果 nodeUid 是叶子节点,往 nodeUid 节点存信息
            res = insertAndSplit(nodeUid, uid, key);
        } else {
            ///(同层查找)在当前节点查找,找不到就在相邻节点查找,返回找到的 key 的 uid 或者邻节点的 uid
            long next = searchNext(nodeUid, key);
            /// 如果不是叶子节点,继续在下一层搜索直到找到对应的叶子节点位置,当叶子节点满时,返回新分裂出的节点位置。
            InsertRes ir = insert(next, uid, key);
            /// 把分裂节点插入到一个非叶子节点上
            if(ir.newNode != 0) {
                /// 分裂出节点,把分裂节点的 key|uid 添加到对应的父节点中。
                /// 如果原来的父节点已满,就会重新分裂出一个父节点,会返回分裂出的父节点。
                res = insertAndSplit(nodeUid, ir.newNode, ir.newKey);
            } else { /// 没有分裂出新节点
                res = new InsertRes();
            }
        }
        return res;
    }

    private InsertRes insertAndSplit(long nodeUid, long uid, long key) throws Exception {
        while(true) {
            Node node = Node.loadNode(this, nodeUid);
            InsertAndSplitRes iasr = node.insertAndSplit(uid, key);
            node.release();
            if(iasr.siblingUid != 0) { /// 插入失败,返回兄弟节点
                nodeUid = iasr.siblingUid;
            } else { /// 插入成功,如果需要分裂节点,res 是新节点的 uid 和 key,如果不需要,res 是 0
                InsertRes res = new InsertRes();
                res.newNode = iasr.newSon;
                res.newKey = iasr.newKey;
                return res;
            }
        }
    }

3. 范围查询

与Node节点类似,不过要先一直定位到对应的叶子节点的位置,再按照Node节点的查询方法进行查询。

    public List<Long> searchRange(long leftKey, long rightKey) throws Exception {
        long rootUid = rootUid();
        //从uid为rootUid的节点开始寻找索引为leftKey的数据的叶子节点的uid
        long leafUid = searchLeaf(rootUid, leftKey);
        List<Long> uids = new ArrayList<>();
        while(true) {
            Node leaf = Node.loadNode(this, leafUid);
            LeafSearchRangeRes res = leaf.leafSearchRange(leftKey, rightKey);
            leaf.release();
            uids.addAll(res.uids);
            if(res.siblingUid == 0) {
                break;
            } else {
                leafUid = res.siblingUid;
            }
        }
        return uids;
    }
    //从uid为nodeUid的节点开始寻找索引为key的数据的uid(直到找到叶子节点)
    private long searchLeaf(long nodeUid, long key) throws Exception {
        Node node = Node.loadNode(this, nodeUid);
        boolean isLeaf = node.isLeaf();
        node.release();
        if(isLeaf) {
            //叶子节点
            return nodeUid;
        } else {
            //找到索引为key的uid,继续往下搜索,直到搜到叶子节点
            long next = searchNext(nodeUid, key);
            return searchLeaf(next, key);
        }
    }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值