214 lines
11 KiB
Markdown
214 lines
11 KiB
Markdown
# Bonus
|
||
|
||
括号里的分值为最高得分,最终得分由 code review 确定。另请注意,总分值不能超过 10 分。
|
||
|
||
## 缓存 *(1.5 pts)*
|
||
|
||
如果将一些常用数据存在内存中,可以省去大量从外存上读取的时间,这就是缓存的意义。
|
||
|
||
此 bonus 根据实现难度和工作量给分。
|
||
|
||
**注:** 若选择实现并发功能,则此 bonus 分不再计入总分。
|
||
|
||
## 中文输入验证 *(1.5 pts)*
|
||
|
||
在实际使用中,书名、作者名等只支持 ASCII 显然是不够的;我们有很多中文的书名。但是,为了贯彻落实文化自信,我们也不允许书名中出现不是汉字的文字。
|
||
|
||
我们把图书相关信息做扩展定义如下:(其他定义不变)
|
||
|
||
- `[BookName]`, `[Author]`:图书名字,图书作者名字;
|
||
- 合法字符集:除不可见字符和英文双引号以外 ASCII 字符,以及汉字和允许的特殊符号;
|
||
- 最大长度:60,一个汉字或特殊符号算作一个字符。
|
||
- `[Keyword]`:图书关键词;
|
||
- 合法字符集:除不可见字符和英文双引号以外 ASCII 字符,以及汉字和允许的特殊符号;
|
||
- 最大长度:60,一个汉字或特殊符号算作一个字符。
|
||
|
||
其中,汉字的定义是:最新 Unicode 标准(Unicode 15.0.0)中指定的 Han Ideographs(汉字)区块内的字。网上能查到的大部分匹配方式都不符合这个要求。例如,它们都不能匹配「𰻝」字(音 biáng,U+30EDD,位于 CJK 扩展 G 区)。所以你需要自行查阅相关文档来找出具体的字汇。
|
||
|
||
允许的特殊符号有:
|
||
|
||
- 间隔号「·」,U+00B7
|
||
- Em Dash「—」,U+2014
|
||
- 括号「(」「)」,U+FF08 U+FF09
|
||
- 冒号「:」,U+FF1A
|
||
|
||
你需要自己了解并手写解析 UTF-8 编码的代码。使用 C++ 标准库解析 UTF-8 不能得分。
|
||
|
||
## Validator *(1.5 pts)*
|
||
|
||
本任务的主要目的是练习一些 C++ 语法的使用。
|
||
|
||
在写 Bookstore 的时候,你一定会写到很多参数的验证:指令需要验证,帐户信息需要验证,图书信息也需要验证。这个任务提供了一个参数验证方法。例如,如果想验证 `quantity` 在 1 到 10 之间且不是 4,就可以写:
|
||
|
||
```cpp
|
||
expect(quantity).ge(1).And.le(10).And.Not().toBe(4);
|
||
```
|
||
|
||
就相当于写:
|
||
|
||
```cpp
|
||
if (!(quantity >= 1 && quantity <= 10 && quantity != 4)) {
|
||
throw std::exception();
|
||
}
|
||
```
|
||
|
||
本任务要求你实现一个这样的 `expect` 函数(写 bookstore 主体的时候这个函数也能用上),若验证不通过则 `throw` 任意值。它只有一个输入参数,其他参数以*链式调用 (method chaining)* 的方式传入。`expect` 函数是多态的。你需要实现以下接口:
|
||
|
||
```cpp
|
||
struct T {};
|
||
struct U : T {};
|
||
T x, y, z;
|
||
U u;
|
||
T &ref = u;
|
||
|
||
// T 的 == 需要有定义,验证是否有 x == y
|
||
expect(x).toBe(y);
|
||
|
||
// 验证 ref 是否为 U 的实例。
|
||
// **注意,此处如果 U 根本不是 T 的子类,则需要编译报错。**
|
||
expect(ref).toBe<U>();
|
||
|
||
// T 的 == 需要有定义,接收任意多个(至少一个)参数,验证 x 是否与参数列表中的至少一个值相等
|
||
expect(x).toBeOneOf(x, y, z, ...);
|
||
|
||
// T 的 <= 或 >= 需要有定义,验证是否有 x <= y 或 x >= y
|
||
expect(x).le(y);
|
||
expect(x).ge(y);
|
||
|
||
// Not() 将后面跟的所有条件取反
|
||
// 意为 x 不能为 1 或 2
|
||
expect(x).Not().toBe(1).Or.toBe(2);
|
||
```
|
||
|
||
如果传入的 x 是 [`std::basic_string<CharT>`](https://en.cppreference.com/w/cpp/string/basic_string)(注意不一定是 `std::string`,点击链接跳转到 C++ Reference),则还需实现以下函数:
|
||
|
||
```cpp
|
||
std::basic_string<CharT> str1, str2;
|
||
|
||
// 验证 str1 中的所有字符都包含于 str2 中
|
||
// 例如 expect(std::string("1024.00")).toBeConsistedOf("42.01")
|
||
// 提示:std::basic_string<CharT>::find()
|
||
expect(str1).consistedOf(str2);
|
||
|
||
// 验证 str1 符合 str2 这个正则表达式
|
||
// 提示:可以使用 std::basic_regex<CharT> 实现
|
||
expect(str1).toMatch(str2);
|
||
```
|
||
|
||
这些方法需要能够任意连接:(中间的 `And`, `Or` 和 `but` 为无意义连词,直接令其为 `*this` 即可):
|
||
|
||
```cpp
|
||
// 意为 x 需要在 10 到 100 之间,且不是 2 3 5 7 11 中的任意一个
|
||
expect(x)
|
||
.ge(10)
|
||
.And.le(100)
|
||
.but.Not().toBeOneOf(2, 3, 5, 7)
|
||
.Or.toBe(11);
|
||
```
|
||
|
||
## 并发 *(10 pts)*
|
||
|
||
对于一个强大的数据库系统来说,并发功能是非常重要的。在本任务中,你需要实现块链的并发功能。
|
||
|
||
从 Wikipedia 上可以找到对于并发的定义:
|
||
|
||
> In computer science, concurrency is the ability of different parts or units of a program, algorithm, or problem to be executed out-of-order or in partial order, without affecting the final outcome.
|
||
|
||
如果想要进一步了解 C++ 中如何实现一个并发的块状链表,你需要:
|
||
|
||
- 学习 C++ 中如何对线程进行操作,可参看 [C++多线程并发基础入门教程](https://zhuanlan.zhihu.com/p/194198073)
|
||
- 参考链表的线程安全实现,可参看 [使用线程安全型双向链表实现简单 LRU Cache 模拟](https://cloud.tencent.com/developer/article/1902797)
|
||
|
||
以上提供的资料仅用于初步了解并发,如果想要实现一个并发的块状链表,你可能还需要搜索、参看更多资料。
|
||
|
||
**Hint:** 简单来说,块状链表应该能够同时处理多个块,主要需要考虑如何分块、并块。如果不同线程同时处理的块恰为同一个块,如何让其中一个线程知道这个块正在被占用?将块正在被占用的信息写回外存显然在时间上是不可接受的。
|
||
|
||
此 bonus 仅是对于块状链表的要求,如果感兴趣,你也可以让你的主体支持高并发操作。
|
||
|
||
**注意:** 准备尝试此任务的同学请务必与助教联系并讨论自己的实现思路。
|
||
|
||
## 快照 *(10 pts)*
|
||
|
||
对于一个强大的数据库系统来说,支持快照与恢复功能是非常重要的。在本任务中,你需要实现一个快照系统,实现任意快照间的切换。
|
||
|
||
一个快照几乎不会占用额外存储空间:
|
||
|
||
- 在一个状态上创建一个快照,应该只会占用很小的额外存储来记录这个快照本身的信息,不会将快照的内容完全复制一遍;
|
||
- 在同样的内容上创建 100 个快照,应该只会占用很小的额外存储,不会使磁盘占用增加 100 倍;
|
||
- 在创建快照之后改动一小部分内容,再创建一个快照,也应该只会占用很小的额外存储,不会使磁盘占用增加一倍。
|
||
|
||
要做到这点,可以利用 [Copy-on-write](https://en.wikipedia.org/wiki/Copy-on-write) 的做法。简单来说,就是令所有块都是不可变的,在试图修改一个块的时候,对这个块做复制,然后在复制出的新块上做修改。但是如果在不快照的时候也对这些块做 copy-on-write 的话,每修改一次数据,就会增加一个新块,这是不可接受的。因此,你需要只在快照的时候做 copy-on-write。
|
||
|
||
本任务引入了一条新的指令 `snapshot`,需要权限等级为 7:
|
||
|
||
- `[SnapshotID]`
|
||
- 合法字符集:大小写英文字母和数字;
|
||
- 最大长度:16;
|
||
- 保证所有 `SnapshotID` 均合法。
|
||
- **创建快照**
|
||
- {7} `snapshot create [SnapshotID]`
|
||
- 在当前状态上创建一个快照,名称为 `SnapshotID`。
|
||
- 保证 `SnapshotID` 不会重复。
|
||
- **还原快照**
|
||
- {7} `snapshot restore [SnapshotID]`
|
||
- 还原到名称为 `SnapshotID` 的快照状态,并清空登录栈。
|
||
- 清空登录栈是为了避免书籍信息被修改所带来的不必要的麻烦;
|
||
- 保证 `SnapshotID` 是存在的。
|
||
- **删除快照**
|
||
- {7} `snapshot delete [SnapshotID]`
|
||
- 删除名称为 `SnapshotID` 的快照状态,不改变当前状态。
|
||
- 其占用的存储空间也需一并释放(下文中说明的情况除外);
|
||
- 保证 `SnapshotID` 是存在的。
|
||
- **合并快照(仅 A 班需要实现)**
|
||
- {7} `snapshot merge [SnapshotID]`
|
||
- 将名称为 `SnapshotID` 的快照状态合并到当前状态(不一定是一个快照)中,并清空登录栈;
|
||
- 只改变当前状态,快照状态不变;
|
||
- 清空登录栈是为了避免书籍信息被修改所带来的不必要的麻烦;
|
||
- 测试数据保证 `SnapshotID` 是存在的。
|
||
- 「合并」的定义是:
|
||
- 日志系统不做合并,保留当前日志;
|
||
- 首先,每个快照都需要记录一个「父节点」的信息,表示这个快照之前的快照;
|
||
- 对于第一个快照来说,其父节点是空状态。
|
||
- 找出当前状态与需要合并的快照状态的最小公共祖先;
|
||
- 即使 LCA 状态所对应的快照被删除了,它的状态也需要被保留。
|
||
- 将当前状态和需要合并的快照中,每个对象(一本书、一个帐户)的状态分别与 LCA 中的状态作合并。
|
||
- 如果存在两侧都修改(不包括删除)了同一个对象的情况,则输出 `Invalid\n` 并不做任何合并;
|
||
- 如果存在两侧都新增了同一个对象的情况(用户以 UserID 为准,书籍以 ISBN 为准),则输出 `Invalid\n` 并不做任何合并;
|
||
- 如果其中至少一侧删除了一个对象(包括两侧删除了同一个对象),则删除这个对象;
|
||
- 如果其中恰有一侧修改了一个对象,则保留修改过的对象;
|
||
- 如果其中恰有一侧新增了一个对象,则保留新增的对象;
|
||
- 数据保证 ISBN 不会被修改。
|
||
- 你可以暂时把所有数据加载到内存,但是合并结束之后需要将这些数据写回文件,并从内存中删除;
|
||
- 实现中不需要考虑时间复杂度。
|
||
|
||
**注意:** 准备尝试此任务的同学请务必与助教联系并讨论自己的实现思路。
|
||
|
||
## 容错 *(10 pts)*
|
||
|
||
程序执行时,我们可能会遇到突然断电等意外情况,导致操作中断。如果此时恰好在对磁盘进行读写操作,那么原本的文件系统就会被破坏,从而造成数据损坏的情况。
|
||
|
||
在 Unix 中常用的文件系统中,删除一个文件可以分为 3 步:
|
||
|
||
1. 删除访问的 entry 指针
|
||
2. 释放对应的 index node
|
||
3. 归还对应的磁盘空间
|
||
|
||
如果操作发生中断,一个常见的恢复方式是从第一条输入指令开始,重新执行一整轮操作,但是时间成本显然难以接受。
|
||
|
||
因此,我们可以实现一个 journaling file system,利用日志来恢复文件,保证文件的数据不被损坏。
|
||
|
||
我们可以考虑把一个指令分为多个 **原子操作** (atomic operation),在 journal 中保存每个原子操作。当程序中断时,我们就可以知道保存了哪些原子操作,进而进行文件恢复。
|
||
|
||
**Hint**:请思考一下在不同步骤发生中断时,应该如何选取合适的原子操作,并通过原子操作来恢复文件。 [reference link](https://en.wikipedia.org/wiki/Journaling_file_system)
|
||
|
||
**注意:** 准备尝试此任务的同学请务必与助教联系并讨论自己的实现思路。
|
||
|
||
## GUI 前端 *(8.5 pts)*
|
||
|
||
提供一个用户友好的图形化前端(Qt、Gtk、Web、Tcl/Tk 等均可)。基本要求为:
|
||
|
||
- 前后端分离,即前端代码不过度侵入主体逻辑;
|
||
- 用户能通过用户友好的图形化界面完成**所有**操作;
|
||
- 提供完整的《系统安装手册》《用户手册》。
|
||
|
||
**注意:** 准备尝试此任务的同学请务必与助教联系并讨论自己的实现思路。 |