【MyDB】3-DataManager数据管理 之 7-DataItem项
代码位于
top/xianghua/mydb/server/utils/Types.java
DataManager的实现
在介绍了数据页面缓存与管理,页面索引以及DataItem后。接下来就可以实现DataManager了。
DataManager是数据库系统中的核心组件,负责管理底层数据的访问,修改和数据处理。是DM直接对外提供方法的类,也实现了对DataItem对象的缓存管理。
为了标识不同的DataItem,因此每个DataItem都有唯一的uid来标识。uid是页号和页内偏移组成的一个8字节无符号整数,页号和偏移各占4字节。
DataItem中Uid的生成与解析
DataItem
在 DataManager
中的存储和管理是通过一个唯一标识符 Uid
来实现的。这个 Uid
是由页面编号 (pgno
) 和页面内偏移量 (offset
) 组成的一个 8 字节无符号整数,其中页号和偏移量各占 4 字节。这里以pgno = 2 和 offset = 0
来演示生成和解析 Uid
的详细过程。
- 生成 Uid 通过将页面编号 (
pgno
) 和偏移量 (offset
) 组合成一个 8 字节无符号整数来生成Uid
。这里使用了位移和按位或运算。
public class Types {
public static long addressToUid(int pgno, short offset) {
long u0 = (long)pgno;
long u1 = (long)offset;
return u0 << 32 | u1;
}
}
2.从 Uid 中提取偏移量 (offset
) 为了从 Uid
中提取出偏移量,需要对 Uid
进行按位与运算。偏移量是 Uid
的低 16 位,通过与 16 位全1(0xFFFF
)进行按位与操作可以提取出偏移量。
通过位运算从 uid
中提取出偏移量。(1L << 16) - 1
创建了一个 16 位全为 1 的掩码(即 0xFFFF
)。通过 uid & ((1L << 16) - 1)
将 uid
与这个掩码进行按位与操作,得到的结果就是 uid
的低 16 位,这部分被认为是偏移量,并将其强制转换为 short
类型。
代码位于
top/xianghua/mydb/server/dm/DataManagerImpl.java 中
short offset = (short)(uid & ((1L << 16) - 1));
- 从 Uid 中提取页面编号 (
pgno
) 提取页面编号则需要将Uid
右移 32 位,以便将高 32 位对齐到低位,然后通过按位与操作提取出页面编号。
uid >>>= 32;
int pgno = (int)(uid & ((1L << 32) - 1));
getForCache()
也是继承自AbstractCache
,只需要从 key 中解析出页号,从 pageCache 中获取到页面,再根据偏移,解析出 DataItem 即可
@Override
protected DataItem getForCache(long uid) throws Exception {
// 从 uid 中提取出偏移量(offset),这是通过位操作实现的,偏移量是 uid 的低16位
short offset = (short) (uid & ((1L << 16) - 1));
// 将 uid 右移32位,以便接下来提取出页面编号(pgno)
uid >>>= 32;
// 从 uid 中提取出页面编号(pgno),页面编号是 uid 的高32位
int pgno = (int) (uid & ((1L << 32) - 1));
// 使用页面缓存(pc)的 getPage(int pgno) 方法根据页面编号获取一个 Page 对象
Page pg = pc.getPage(pgno);
// 使用 DataItem 接口的静态方法 parseDataItem(Page pg, short offset, DataManagerImpl dm)
// 根据获取到的 Page 对象、偏移量和当前的 DataManagerImpl 对象(this)解析出一个 DataItem 对象,并返回这个对象
return DataItem.parseDataItem(pg, offset, this);
}
releaseForCache()
DataItem 缓存释放,需要将 DataItem 写回数据源,由于对文件的读写是以页为单位进行的,只需要将 DataItem 所在的页 release 即可:
@Override
protected void releaseForCache(DataItem di) {
di.page().release();
}
DataManager.java 接口–初始化DataManager
代码位于
top/xianghua/mydb/server/dm/DataManager.java
对于DataManager
文件的创建有两种流程,一种是从已有文件创建DataManager
**,另外一种是从空文件创建DataManager
。**对于两者的不同主要在于第一页的初始化和校验问题:
- 从空文件创建首先需要对第一页进行初始化
- 而从已有文件创建,则需要对第一页进行校验,来判断是否需要执行恢复流程,并重新对第一页生成随机字节
从空文件创建DataManager
-
创建PageCache,Logger 对象,根据这些对象创建新的DataManager
-
调用DataManagerImpl中的
initPageOne()
方法,初始化第一页此处可参考3-数据页管理中的首页管理。copy过来如下
数据库文件的第一页,通常用作一些特殊用途,比如存储一些元数据,用来启动检查什么的。MYDB 的第一页,只是用来做启动检查。具体的原理是,在每次数据库启动时,会生成一串随机字节,存储在 100 ~ 107 字节。在数据库正常关闭时,会将这串字节,拷贝到第一页的 108 ~ 115 字节。
这样数据库在每次启动时,就会检查第一页两处的字节是否相同,以此来判断上一次是否正常关闭。如果是异常关闭,就需要执行数据的恢复流程。
public static DataManager create(String path, long mem, TransactionManager tm) {
PageCache pc = PageCache.create(path, mem);
Logger lg = Logger.create(path);
DataManagerImpl dm = new DataManagerImpl(pc, lg, tm);
dm.initPageOne();
return dm;
}
其中initPageOne()方法如下
DataManagerImpl.initPageOne()
// 在创建文件时初始化PageOne
void initPageOne() {
int pgno = pc.newPage(PageOne.InitRaw());
assert pgno == 1;
try {
pageOne = pc.getPage(pgno);
} catch (Exception e) {
Panic.panic(e);
}
pc.flushPage(pageOne);
}
可以看到,其对首页初始化的过程如下:
-
调用
PageCahcheImpl
类的newPage
方法,接收一个byte[]数组top/xianghua/mydb/server/dm/pageCache/PageCacheImpl.java
public int newPage(byte[] initData) { int pgno = pageNumbers.incrementAndGet(); Page pg = new PageImpl(pgno, initData, null); flush(pg); return pgno; }
-
接收的byte数组为
PageOne.InitRaw()
方法初始化后的添加校验字节的数组:top/xianghua/mydb/server/dm/page/PageOne.java
public static byte[] InitRaw() { byte[] raw = new byte[PageCache.PAGE_SIZE]; setVcOpen(raw); return raw; }
从已有文件创建DataManager
- 打开PageCache和Logger实例,并根据这些创建一个DataManagerImpl实例。
loadCheckPageOne
方法加载并检查PageOne,检查失败则进行恢复操作fillPageIndex
方法,填充6-页面索引中提到的页面索引。遍历第二页开始的每一页,将每页的空闲空间大小都添加到PageIndex中。- 将PageOne写入磁盘
public static DataManager open(String path, long mem, TransactionManager tm) {
PageCache pc = PageCache.open(path, mem);
Logger lg = Logger.open(path);
DataManagerImpl dm = new DataManagerImpl(pc, lg, tm);
if(!dm.loadCheckPageOne()) {
Recover.recover(tm, lg, pc);
}
dm.fillPageIndex();
PageOne.setVcOpen(dm.pageOne);
dm.pc.flushPage(dm.pageOne);
return dm;
}
loadCheckPageOne
调用PageOne的checkVc方法校验
// 在打开已有文件时时读入PageOne,并验证正确性
boolean loadCheckPageOne() {
try {
pageOne = pc.getPage(1);
} catch (Exception e) {
Panic.panic(e);
}
return PageOne.checkVc(pageOne);
}
fillPageIndex
初始化pageIndex
// 初始化pageIndex
void fillPageIndex() {
int pageNumber = pc.getPageNumber();
for(int i = 2; i <= pageNumber; i ++) {
Page pg = null;
try {
pg = pc.getPage(i);
} catch (Exception e) {
Panic.panic(e);
}
pIndex.add(pg.getPageNumber(), PageX.getFreeSpace(pg));
pg.release();
}
}
DataMagerImpl——DataManager接口实现类
DM层主要提供三个功能供上层使用。分别是读,插入和修改。
由于修改是通过读出的 DataItem
实现的,也就是说 DataManager
只需要 read()
和 insert()
方法;
read()
根据uid从缓存中获取对应的DataItem对象
read
方法如下。
@Override
public DataItem read(long uid) throws Exception {
DataItemImpl di = (DataItemImpl)super.get(uid);
if(!di.isValid()) {
di.release();
return null;
}
return di;
}
发现,主要过程为
-
调用
AbstractCache
的get
方法获取资源-
AbstractCache
的get
方法需要实现getForCache 抽象方法。 -
因此,
DataManagerImpl
中的getForCache
方法如下:当在缓存中没有找到数据时,会根据DataItem的UID,获取DataItem对象
@Override protected DataItem getForCache(long uid) throws Exception { short offset = (short)(uid & ((1L << 16) - 1)); uid >>>= 32; int pgno = (int)(uid & ((1L << 32) - 1)); Page pg = pc.getPage(pgno); return DataItem.parseDataItem(pg, offset, this); }
-
insert()
在这里插入图片描述
DataItem.wrapDataItemRaw(data)
包装raw为DataItem格式PageIndex.select
根据给定空间大小选择合适的PageInfo对象- 调用
Recover.insertLog
方法,创建插入日志 - 页面中插入包装好的数据项
PageX.insert(pg, raw)
@Override
public long insert(long xid, byte[] data) throws Exception {
// 将输入的数据包装成DataItem的原始格式
byte[] raw = DataItem.wrapDataItemRaw(data);
if(raw.length > PageX.MAX_FREE_SPACE) {
throw Error.DataTooLargeException;
}
PageInfo pi = null;
for(int i = 0; i < 5; i ++) {
// 从页面索引中选择一个可以容纳新数据项的页面
pi = pIndex.select(raw.length);
if (pi != null) {
// 如果找到了合适的页面,跳出循环
break;
} else {
// 如果没有找到合适的页面,创建一个新的页面,并将其添加到页面索引中
int newPgno = pc.newPage(PageX.initRaw());
pIndex.add(newPgno, PageX.MAX_FREE_SPACE);
}
}
if(pi == null) {
throw Error.DatabaseBusyException;
}
// 初始化一个页面对象
Page pg = null;
int freeSpace = 0;
try {
// 获取页面信息对象中的页面
pg = pc.getPage(pi.pgno);
// 生成插入日志
byte[] log = Recover.insertLog(xid, pg, raw);
// 将日志写入日志文件
logger.log(log);
// 在页面中插入新的数据项,并获取其在页面中的偏移量
short offset = PageX.insert(pg, raw);
// 释放页面
pg.release();
// 返回新插入的数据项的唯一标识符
return Types.addressToUid(pi.pgno, offset);
} finally {
// 将取出的pg重新插入pIndex
if(pg != null) {
pIndex.add(pi.pgno, PageX.getFreeSpace(pg));
} else {
pIndex.add(pi.pgno, freeSpace);
}
}
}
DataItem.wrapDataItemRaw(data)
包装raw为DataItem格式
/**
* 返回一个完整的 DataItem 结构数据
* dataItem 结构如下:
* [ValidFlag] [DataSize] [Data]
* ValidFlag 1字节,0为合法,1为非法
* DataSize 2字节,标识Data的长度
* @param raw
* @return
*/
public static byte[] wrapDataItemRaw(byte[] raw) {
byte[] valid = new byte[1]; //证明此时为非法数据
byte[] size = Parser.short2Byte((short)raw.length); //计算数据字节大小
return Bytes.concat(valid, size, raw); //拼接DataItem 结构数据
}
PageIndex.select
根据给定空间大小选择合适的PageInfo对象
/**
* 根据给定的空间大小选择一个 PageInfo 对象。
*
* @param spaceSize 需要的空间大小
* @return 一个 PageInfo 对象,其空闲空间大于或等于给定的空间大小。如果没有找到合适的 PageInfo,返回 null。
*/
public PageInfo select(int spaceSize) {
lock.lock(); // 获取锁,确保线程安全
try {
int number = spaceSize / THRESHOLD; // 计算需要的空间大小对应的区间编号
// 此处+1主要为了向上取整
/*
1、假需要存储的字节大小为5168,此时计算出来的区间号是25,但是25*204=5100显然是不满足条件的
2、此时向上取整找到 26,而26*204=5304,是满足插入条件的
*/
if (number < INTERVALS_NO) number++; // 如果计算出的区间编号小于总的区间数,编号加一
while (number <= INTERVALS_NO) { // 从计算出的区间编号开始,向上寻找合适的 PageInfo
if (lists[number].size() == 0) { // 如果当前区间没有 PageInfo,继续查找下一个区间
number++;
continue;
}
return lists[number].remove(0); // 如果当前区间有 PageInfo,返回第一个 PageInfo,并从列表中移除
}
return null; // 如果没有找到合适的 PageInfo,返回 null
} finally {
lock.unlock(); // 释放锁
}
}
- 调用
Recover.insertLog
方法,创建插入日志
// 定义一个静态方法,用于创建插入日志
public static byte[] insertLog(long xid, Page pg, byte[] raw) {
// 创建一个表示日志类型的字节数组,并设置其值为LOG_TYPE_INSERT
byte[] logTypeRaw = {LOG_TYPE_INSERT};
// 将事务ID转换为字节数组
byte[] xidRaw = Parser.long2Byte(xid);
// 将页面编号转换为字节数组
byte[] pgnoRaw = Parser.int2Byte(pg.getPageNumber());
// 获取页面的第一个空闲空间的偏移量,并将其转换为字节数组
byte[] offsetRaw = Parser.short2Byte(PageX.getFSO(pg));
// 将所有字节数组连接在一起,形成一个完整的插入日志,并返回这个日志
return Bytes.concat(logTypeRaw, xidRaw, pgnoRaw, offsetRaw, raw);
}
- 页面中插入包装好的数据项
PageX.insert(pg, raw)
// 将raw插入pg中,返回插入位置
public static short insert(Page pg, byte[] raw) {
pg.setDirty(true); // 将pg的dirty标志设置为true,表示pg的数据已经被修改
short offset = getFSO(pg.getData()); // 获取pg的空闲空间偏移量
System.arraycopy(raw, 0, pg.getData(), offset, raw.length); // 将raw的数据复制到pg的数据中的offset位置
setFSO(pg.getData(), (short) (offset + raw.length)); // 更新pg的空闲空间偏移量
return offset; // 返回插入位置
}
close()
DataManager 正常关闭时,需要执行缓存和日志的关闭流程,还需要设置第一页的字节校验:
@Override
public void close() {
super.close();
logger.close();
PageOne.setVcClose(pageOne);
pageOne.release();
pc.close();
}