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);
}
}