/* Copyright (c) 2021 Xie Meiyi(xiemeiyi@hust.edu.cn) and OceanBase and/or its affiliates. All rights reserved. miniob is licensed under Mulan PSL v2. You can use this software according to the terms and conditions of the Mulan PSL v2. You may obtain a copy of Mulan PSL v2 at: http://license.coscl.org.cn/MulanPSL2 THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. See the Mulan PSL v2 for more details. */ // // Created by Meiyi & Longda on 2021/4/13. // #pragma once #include #include #include "storage/buffer/disk_buffer_pool.h" #include "storage/trx/latch_memo.h" #include "storage/record/record.h" #include "common/lang/bitmap.h" class ConditionFilter; class RecordPageHandler; class Trx; class Table; /** * @brief 这里负责管理在一个文件上表记录(行)的组织/管理 * @defgroup RecordManager * * @file record_manager.h * * @details 表记录管理的内容包括如何在文件上存放、读取、检索。也就是记录的增删改查。 * 这里的文件都会被拆分成页面,每个页面都有一样的大小。更详细的信息可以参考BufferPool。 * 按照BufferPool的设计,第一个页面用来存放BufferPool本身的元数据,比如当前文件有多少页面、已经分配了多少页面、 * 每个页面的分配状态等。所以第一个页面对RecordManager来说没有作用。RecordManager 本身没有再单独拿一个页面 * 来存放元数据,每一个页面都存放了一个页面头信息,也就是每个页面都有 RecordManager 的元数据信息,可以参考 * PageHeader,这虽然有点浪费但是做起来简单。 * * 对单个页面来说,最开始是一个页头,然后接着就是一行行记录(会对齐)。 * 如何标识一个记录,或者定位一个记录? * 使用RID,即record identifier。使用 page num 表示所在的页面,slot num 表示当前在页面中的位置。因为这里的 * 记录都是定长的,所以根据slot num 可以直接计算出记录的起始位置。 * 问题1:那么如果记录不是定长的,还能使用 slot num 吗? * 问题2:如何更有效地存放不定长数据呢? * 问题3:如果一个页面不能存放一个记录,那么怎么组织记录存放效果更好呢? * * 按照上面的描述,这里提供了几个类,分别是: * - RecordFileHandler:管理整个文件/表的记录增删改查 * - RecordPageHandler:管理单个页面上记录的增删改查 * - RecordFileScanner:可以用来遍历整个文件上的所有记录 * - RecordPageIterator:可以用来遍历指定页面上的所有记录 * - PageHeader:每个页面上都会记录的页面头信息 */ /** * @brief 数据文件,按照页面来组织,每一页都存放一些记录/数据行 * @ingroup RecordManager * @details 每一页都有一个这样的页头,虽然看起来浪费,但是现在就简单的这么做 * 从这个页头描述的信息来看,当前仅支持定长行/记录。如果要支持变长记录, * 或者超长(超出一页)的记录,这么做是不合适的。 */ struct PageHeader { int32_t record_num; ///< 当前页面记录的个数 int32_t record_capacity; ///< 最大记录个数 int32_t record_real_size; ///< 每条记录的实际大小 int32_t record_size; ///< 每条记录占用实际空间大小(可能对齐) int32_t first_record_offset; ///< 第一条记录的偏移量 }; /** * @brief 遍历一个页面中每条记录的iterator * @ingroup RecordManager */ class RecordPageIterator { public: RecordPageIterator(); ~RecordPageIterator(); /** * @brief 初始化一个迭代器 * * @param record_page_handler 负责某个页面上记录增删改查的对象 * @param start_slot_num 从哪个记录开始扫描,默认是0 */ void init(RecordPageHandler &record_page_handler, SlotNum start_slot_num = 0); bool has_next(); RC next(Record &record); bool is_valid() const { return record_page_handler_ != nullptr; } private: RecordPageHandler *record_page_handler_ = nullptr; PageNum page_num_ = BP_INVALID_PAGE_NUM; common::Bitmap bitmap_; ///< bitmap 的相关信息可以参考 RecordPageHandler 的说明 SlotNum next_slot_num_ = 0; ///< 当前遍历到了哪一个slot }; /** * @brief 负责处理一个页面中各种操作,比如插入记录、删除记录或者查找记录 * @ingroup RecordManager * @details 当前定长记录模式下每个页面的组织大概是这样的: * | PageHeader | record allocate bitmap | * |------------|------------------------| * | record1 | record2 | ..... | recordN | */ class RecordPageHandler { public: RecordPageHandler() = default; ~RecordPageHandler(); /** * @brief 初始化 * * @param buffer_pool 关联某个文件时,都通过buffer pool来做读写文件 * @param page_num 当前处理哪个页面 * @param readonly 是否只读。在访问页面时,需要对页面加锁 */ RC init(DiskBufferPool &buffer_pool, PageNum page_num, bool readonly); /** * @brief 数据库恢复时,与普通的运行场景有所不同,不做任何并发操作,也不需要加锁 * * @param buffer_pool 关联某个文件时,都通过buffer pool来做读写文件 * @param page_num 操作的页面编号 */ RC recover_init(DiskBufferPool &buffer_pool, PageNum page_num); /** * @brief 对一个新的页面做初始化 * * @param buffer_pool 关联某个文件时,都通过buffer pool来做读写文件 * @param page_num 当前处理哪个页面 * @param record_size 每个记录的大小 */ RC init_empty_page(DiskBufferPool &buffer_pool, PageNum page_num, int record_size); /** * @brief 操作结束后做的清理工作,比如释放页面、解锁 */ RC cleanup(); /** * @brief 插入一条记录 * * @param data 要插入的记录 * @param rid 如果插入成功,通过这个参数返回插入的位置 */ RC insert_record(const char *data, RID *rid); /** * @brief 数据库恢复时,在指定位置插入数据 * * @param data 要插入的数据行 * @param rid 插入的位置 */ RC recover_insert_record(const char *data, const RID &rid); /** * @brief 删除指定的记录 * * @param rid 要删除的记录标识 */ RC delete_record(const RID *rid); /** * @brief 获取指定位置的记录数据 * * @param rid 指定的位置 * @param rec 返回指定的数据。这里不会将数据复制出来,而是使用指针,所以调用者必须保证数据使用期间受到保护 */ RC get_record(const RID *rid, Record *rec); PageNum get_page_num() const; /** * @brief 当前页面是否已经没有空闲位置插入新的记录 */ bool is_full() const; protected: char *get_record_data(SlotNum slot_num) { return frame_->data() + page_header_->first_record_offset + (page_header_->record_size * slot_num); } protected: DiskBufferPool *disk_buffer_pool_ = nullptr; ///< 当前操作的buffer pool(文件) bool readonly_ = false; ///< 当前的操作是否都是只读的 Frame *frame_ = nullptr; ///< 当前操作页面关联的frame(frame的更多概念可以参考buffer pool和frame) PageHeader *page_header_ = nullptr; ///< 当前页面上页面头 char *bitmap_ = nullptr; ///< 当前页面上record分配状态信息bitmap内存起始位置 private: friend class RecordPageIterator; }; /** * @brief 管理整个文件中记录的增删改查 * @ingroup RecordManager * @details 整个文件的组织格式请参考该文件中最前面的注释 */ class RecordFileHandler { public: RecordFileHandler() = default; ~RecordFileHandler(); /** * @brief 初始化 * * @param buffer_pool 当前操作的是哪个文件 */ RC init(DiskBufferPool *buffer_pool); /** * @brief 关闭,做一些资源清理的工作 */ void close(); /** * @brief 从指定文件中删除标识符为rid的记录 */ RC delete_record(const RID *rid); /** * @brief 插入一个新的记录到指定文件中,data为指向新纪录内容的指针,返回该记录的标识符rid */ RC insert_record(const char *data, int record_size, RID *rid); RC recover_insert_record(const char *data, int record_size, const RID &rid); /** * @brief 获取指定文件中标识符为rid的记录内容到rec指向的记录结构中 * @param page_handler[in] * 访问记录时,会拿住一些资源不释放,比如页面锁,使用这个对象保存相关的资源,并在析构时会自动释放 * @param rid 想要获取的记录ID * @param readonly 获取的记录是只读的还是需要修改的 * @param rec[out] 通过这个参数返回获取到的记录 * @note rec 参数返回的记录并不会复制数据内存。page_handler 对象会拿着相关的资源,比如 pin 住页面和加上页面锁。 * 如果page_handler 释放了,那也不能再访问rec对象了。 */ RC get_record(RecordPageHandler &page_handler, const RID *rid, bool readonly, Record *rec); /** * @brief 与get_record类似,访问某个记录,并提供回调函数来操作相应的记录 * * @param rid 想要访问的记录ID * @param readonly 是否会修改记录 * @param visitor 访问记录的回调函数 */ RC visit_record(const RID &rid, bool readonly, std::function visitor); private: /** * @brief 初始化记录当前没有填满记录的页面 */ RC init_free_pages(); private: DiskBufferPool *disk_buffer_pool_ = nullptr; std::unordered_set free_pages_; ///< 没有填充满的页面集合 common::Mutex lock_; ///< 当编译时增加-DCONCURRENCY=ON 选项时,才会真正的支持并发 }; /** * @brief 遍历某个文件中所有记录 * @ingroup RecordManager * @details 遍历所有的页面,同时访问这些页面中所有的记录 */ class RecordFileScanner { public: RecordFileScanner() = default; ~RecordFileScanner(); /** * @brief 打开一个文件扫描。 * @details 如果条件不为空,则要对每条记录进行条件比较,只有满足所有条件的记录才被返回 * @param table 遍历的哪张表 * @param buffer_pool 访问的文件 * @param readonly 当前是否只读操作。访问数据时,需要对页面加锁。比如 * 删除时也需要遍历找到数据,然后删除,这时就需要加写锁 * @param condition_filter 做一些初步过滤操作 */ RC open_scan(Table *table, DiskBufferPool &buffer_pool, Trx *trx, bool readonly, ConditionFilter *condition_filter); /** * @brief 关闭一个文件扫描,释放相应的资源 */ RC close_scan(); /** * @brief 判断是否还有数据 * @details 判断完成后调用next获取下一条数据 */ bool has_next(); /** * @brief 获取下一条记录 * @details 获取下一条记录之前先调用has_next()判断是否还有数据 */ RC next(Record &record); private: /** * @brief 获取下一条记录 */ RC fetch_next_record(); /** * @brief 获取一个页面内的下一条记录 */ RC fetch_next_record_in_page(); private: // TODO 对于一个纯粹的record遍历器来说,不应该关心表和事务 Table *table_ = nullptr; ///< 当前遍历的是哪张表。这个字段仅供事务函数使用,如果设计合适,可以去掉 DiskBufferPool *disk_buffer_pool_ = nullptr; ///< 当前访问的文件 Trx *trx_ = nullptr; ///< 当前是哪个事务在遍历 bool readonly_ = false; ///< 遍历出来的数据,是否可能对它做修改 BufferPoolIterator bp_iterator_; ///< 遍历buffer pool的所有页面 ConditionFilter *condition_filter_ = nullptr; ///< 过滤record RecordPageHandler record_page_handler_; RecordPageIterator record_page_iterator_; ///< 遍历某个页面上的所有record Record next_record_; ///< 获取的记录放在这里缓存起来 };