DataManager
的实现
DataManager
是数据库系统中的核心组件,负责管理底层数据的访问、修改和事务处理。它是 DM
层直接对外提供方法的类,用来对上层其他模块提供数据操作的API的,同时也实现了对 DataItem
对象的缓存管理。DataItem
存储的 key
是由页号和页内偏移组成的一个 8 字节无符号整数,页号和偏移各占 4 字节。
DataManager
的核心功能
- 数据缓存与管理:
DataManager
通过缓存DataItem
对象,提供高效的数据访问。DataItem
的key
是一个 8 字节无符号整数,其中页号和页内偏移各占 4 字节。这种设计允许快速定位和访问数据,减少了对底层存储的频繁访问。 - 数据访问与操作:
DataManager
提供了读取、插入和修改等数据操作方法。通过这些方法,数据库的上层模块可以方便地对数据进行操作。 - 事务管理:
DataManager
支持事务管理,确保数据操作的原子性。事务管理能够保证在事务提交或回滚时,数据的一致性和完整性。 - 日志记录与恢复:
DataManager
在数据修改操作前后执行日志记录,以确保数据的安全性和可靠性,并支持系统崩溃后的数据恢复。 - 页面索引管理:
DataManager
还实现了页面索引管理功能,通过页面索引,可以快速定位到合适的空闲空间,提高数据插入的效率和性能。 - 文件初始化与校验:
DataManager
在创建和打开数据库文件时,进行文件的初始化和校验,确保文件的正确性和完整性。
DataManager
的uid的生成与解析
DataItem
在 DataManager
中的存储和管理是通过一个唯一标识符 Uid
来实现的。这个 Uid
是由页面编号 (pgno
) 和页面内偏移量 (offset
) 组成的一个 8 字节无符号整数,其中页号和偏移量各占 4 字节。高4字节的32位表示DataItem
存储的Page
的页号;低4字节的32位中只有低16位有意义,这16位表示DataItem
存储的Page
中的页内偏移,而高16位无意义 , 这里以pgno = 2 和 offset = 0
来演示生成和解析 Uid
的详细过程。
1、生成 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; // 左移32位表示页号,按位或运算将页号和偏移量合并成一个Uid}
}
按位或:有1为1;双0为0
2、从 Uid 中提取偏移量 (**offset**
) 为了从 Uid
中提取出偏移量,需要对 Uid
进行按位与运算。偏移量是 Uid
的低 16 位,通过与 16 位全1(0xFFFF
)进行按位与操作可以提取出偏移量。(双1为1;其余为0)
// 提取偏移量,偏移量占 Uid 的低16位
short offset = (short) (uid & ((1L << 16) - 1)); // 按位与操作提取出低16位的偏移量
3、从 Uid 中提取页面编号 (**pgno**
) 提取页面编号则需要将 Uid
右移 32 位,以便将高 32 位对齐到低位,然后通过按位与操作提取出页面编号。
// 右移32位,将高32位对齐到低位
uid >>>= 32;
// 提取页面编号,页面编号占 Uid 的高32位
int pgno = (int) (uid & ((1L << 32) - 1)); // 按位与操作提取出页面编号
编码实现
DM结构定义
public class DataManagerImpl extends AbstractCache<DataItem> implements DataManager {TransactionManager tm; //TM事务管理器PageCache pc; //PC页面缓存Logger logger; //数据库日志PageIndex pIndex; //页面索引Page pageOne; //第一页
创建新的DM(构造方法)
public DataManagerImpl(PageCache pc, Logger logger, TransactionManager tm) {super(0);this.pc = pc;this.logger = logger;this.tm = tm;this.pIndex = new PageIndex();}
创建/打开DM
/*** 创建页缓存(PageCache):通过调用PageCache.create(path, mem)方法,创建一个页缓存对象pc。path参数指定了数据存储的路径,mem参数指定了内存大小。* 创建日志记录器(Logger):通过调用Logger.create(path)方法,创建一个日志记录器对象lg。path参数同样指定了数据存储的路径。* 创建数据管理器实现类(DataManagerImpl)实例:使用创建的页缓存对象pc、日志记录器对象lg和事务管理器对象tm作为参数,创建一个数据管理器实现类DataManagerImpl的实例dm。* 初始化页一(PageOne):调用dm.initPageOne()方法,初始化数据管理器中的页一。页一是一个特殊的页,通常用于存储数据库的元数据或配置信息。* 返回数据管理器实例:将创建并初始化好的DataManagerImpl实例dm作为返回值,返回给调用者。这是数据库打开过程中的初始化阶段,其中创建了数据管理器所需的核心组件,并对关键的页进行了初始化操作。* @param path* @param mem* @param tm* @return*/public static DataManager create(String path, long mem, TransactionManager tm) {PageCache pc = PageCache.create(path, mem); //111111Logger lg = Logger.create(path);DataManagerImpl dm = new DataManagerImpl(pc, lg, tm);dm.initPageOne();return dm;}//111111。缓存public static PageCacheImpl create(String path, long memory) {//1、根据路径创建db文件File f = new File(path+PageCacheImpl.DB_SUFFIX);try {//2、判断文件是否能创建成功if(!f.createNewFile()) {Panic.panic(Error.FileExistsException);}} catch (Exception e) {Panic.panic(e);}//3、文件是否可读写if(!f.canRead() || !f.canWrite()) {Panic.panic(Error.FileCannotRWException);}//4、创建NIO管道和随机文件对象操作类创建的文件FileChannel fc = null;RandomAccessFile raf = null;try {raf = new RandomAccessFile(f, "rw");fc = raf.getChannel();} catch (FileNotFoundException e) {Panic.panic(e);}//5、创建PageCacheImpl对象return new PageCacheImpl(raf, fc, (int)memory/PAGE_SIZE);}public static DataManager open(String path, long mem, TransactionManager tm) {PageCache pc = PageCache.open(path, mem);//2222Logger 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;}//222222222222public static PageCacheImpl open(String path, long memory) {File f = new File(path+PageCacheImpl.DB_SUFFIX);if(!f.exists()) { //判断文件是否存在Panic.panic(Error.FileNotExistsException);}if(!f.canRead() || !f.canWrite()) {Panic.panic(Error.FileCannotRWException);}FileChannel fc = null;RandomAccessFile raf = null;try {raf = new RandomAccessFile(f, "rw");fc = raf.getChannel();} catch (FileNotFoundException e) {Panic.panic(e);}return new PageCacheImpl(raf, fc, (int)memory/PAGE_SIZE);}
如上两种不同方式需要注意:
-
从空文件创建需要对第一页进行初始化
-
从已有文件创建(即打开),需要对第一页进行校验从而判断是否需要执行恢复流程,并重新对第一页生成随机字节
初始化PageIndex
// 初始化pageIndexvoid fillPageIndex() {//获取当前的pageCache中的页面数量int pageNumber = pc.getPageNumber();//遍历从第二页开始的每一页for(int i = 2; i <= pageNumber; i ++) {Page pg = null;try {//获取第i页pg = pc.getPage(i);} catch (Exception e) {Panic.panic(e);}//获取第i页的空闲空间大小,将第i页的页面编号和空闲空间大小添加到 PageIndex 中pIndex.add(pg.getPageNumber(), PageX.getFreeSpace(pg));pg.release();}}
这个方法的主要目的是在数据库打开时,为数据管理器构建一个页索引,以便后续的数据操作可以快速定位和访问页
加载并检查PageOne
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
的核心方法
- 读取数据:
read()
- 根据
Uid
从缓存中获取DataItem
,并校验其有效性。DataItem
的Uid
是由页号和页内偏移组成的一个 8 字节无符号整数。
@Override
public DataItem read(long uid) throws Exception {//从缓存页面中读取到DataItemImplDataItemImpl di = (DataItemImpl) super.get(uid); // 若缓存中不存在则调用 getForCache() 方法//校验di是否有效if (!di.isValid()) {// 无效释放缓存di.release();return null;}return di;
}
2.插入数据:insert()
- 在
PageIndex
中选择一个合适的页面进行插入操作,记录插入日志,并返回插入位置的偏移。插入的位置和页面信息都是通过页号和偏移量进行管理的。
@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;// 尝试5次找到一个可以容纳新数据项的页面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;// 初始化空闲空间大小为0int 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 {// 将页面重新添加到页面索引中if (pg != null) {pIndex.add(pi.pgno, PageX.getFreeSpace(pg));} else {pIndex.add(pi.pgno, freeSpace);}}
}/*** 返回一个完整的 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 结构数据
}/*** 根据给定的空间大小选择一个 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) { // 从计算出的区间编号开始,向上寻找合适的 PageInfoif (lists[number].size() == 0) { // 如果当前区间没有 PageInfo,继续查找下一个区间number++;continue;}return lists[number].remove(0); // 如果当前区间有 PageInfo,返回第一个 PageInfo,并从列表中移除}return null; // 如果没有找到合适的 PageInfo,返回 null
} finally {lock.unlock(); // 释放锁
}
}// 定义一个静态方法,用于创建插入日志
public static byte[] insertLog(long xid, Page pg, byte[] raw) {// 创建一个表示日志类型的字节数组,并设置其值为LOG_TYPE_INSERTbyte[] 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);
}// 将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; // 返回插入位置
}
3.关闭 DataManager:close()
- 正常关闭时,执行缓存和日志的关闭流程,并设置第一页的字节校验。
@Override
public void close() {super.close();logger.close();PageOne.setVcClose(pageOne);pageOne.release();pc.close();
}
DataManager的初始化
- 从空文件创建:
create()
- 初始化
PageCache
和Logger
,并初始化第一页。
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;
}
- 从已有文件打开:
open()
- 加载并检查第一页,必要时执行恢复操作,并填充
PageIndex
。
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;
}
总结
DataItem
和 DataManager
是数据库系统中数据管理的关键组件。DataItem
提供了数据的存储和访问接口,支持数据修改和事务管理。而 DataManager
负责管理底层数据的访问、缓存、事务处理和日志记录,通过这些功能的实现,为上层模块提供了安全、高效的数据操作接口。两者的协作使得数据库系统能够以高效、可靠的方式管理和操作底层数据,确保数据的安全性和一致性。