【MyDB】3-DataManager数据管理 之 8-DataManager


代码位于

top/xianghua/mydb/server/utils/Types.java

DataManager的实现

在介绍了数据页面缓存与管理,页面索引以及DataItem后。接下来就可以实现DataManager了。

DataManager是数据库系统中的核心组件,负责管理底层数据的访问,修改和数据处理。是DM直接对外提供方法的类,也实现了对DataItem对象的缓存管理。

为了标识不同的DataItem,因此每个DataItem都有唯一的uid来标识。uid是页号和页内偏移组成的一个8字节无符号整数,页号和偏移各占4字节。

DataItem中Uid的生成与解析

DataItemDataManager 中的存储和管理是通过一个唯一标识符 Uid 来实现的。这个 Uid 是由页面编号 (pgno) 和页面内偏移量 (offset) 组成的一个 8 字节无符号整数,其中页号和偏移量各占 4 字节。这里以pgno = 2 和 offset = 0来演示生成和解析 Uid 的详细过程。

  1. 生成 Uid 通过将页面编号 (pgno) 和偏移量 (offset) 组合成一个 8 字节无符号整数来生成 Uid。这里使用了位移和按位或运算。

types.createuid.png

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 中

datamanger.offset.png

short offset = (short)(uid & ((1L << 16) - 1));
  1. 从 Uid 中提取页面编号 (pgno) 提取页面编号则需要将 Uid 右移 32 位,以便将高 32 位对齐到低位,然后通过按位与操作提取出页面编号。

datamanager.pgno.png

        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。**对于两者的不同主要在于第一页的初始化和校验问题:

  1. 从空文件创建首先需要对第一页进行初始化
  2. 而从已有文件创建,则需要对第一页进行校验,来判断是否需要执行恢复流程,并重新对第一页生成随机字节

从空文件创建DataManager

  1. 创建PageCache,Logger 对象,根据这些对象创建新的DataManager

  2. 调用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);
}

可以看到,其对首页初始化的过程如下:

  1. 调用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;
        }
    
  2. 接收的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

  1. 打开PageCache和Logger实例,并根据这些创建一个DataManagerImpl实例。
  2. loadCheckPageOne方法加载并检查PageOne,检查失败则进行恢复操作
  3. fillPageIndex方法,填充6-页面索引中提到的页面索引。遍历第二页开始的每一页,将每页的空闲空间大小都添加到PageIndex中。
  4. 将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;
    }

发现,主要过程为

  • 调用AbstractCacheget方法获取资源

    • AbstractCacheget方法需要实现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();
}

参考资料

MYDB 5. 页面索引与 DM 的实现 | 信也のブログ (shinya.click)

DM 的实现 | EasyDB (blockcloth.cn)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值