不用数据库实现数据库功能
简略版本
阶段1: 无事务, 单线程, 仅存在于内存的数据库.
该状态下的数据库, 其实就是一个”索引结构”+”语法分析器”.
语法分析器分析SQL语句, 然后根据逻辑, 去执行相应的操作.
索引结构则是用来快速查询.
由于该版本仅存在于内存, 所以只要你会一些常见的索引算法, 即可完成, 可以称之为”简易内存数据库”.
数据结构
如你会B+树算法, 就可以实现一个B+树, Bt.
它实现了两个接口, Bt.Insert(key, value) -> void, Bt.Search(key) -> value.
语法分析器
再实现一个”语法分析器”.
1 如来了一条语句”Insert into student value (tony, 22, 123)”.
2 ”语法分析器”分析该语句, 将value包裹一下, 选取一个该value的键值key.
然后调用 Bt.Insert(key, value).
3 之后执行”Read from student …” 其实也就是分析一下, 然后执行Bt.Search(key).
该版本数据库完成.
阶段2: 无事务, 单线程, 不可靠的磁盘数据库
“磁盘”表示该版本将信息存放在磁盘上.
“不可靠”表示, 当数据库被非正常结束时, 不保证重启后, 数据库内容还会正确.
思路描述
该版本也非常简单, 直接在版本1上修改.
可以这样, 如你索引结构的最小单位为Unit, (如B+树的每个节点就是一个Unit).
你将Unit编码成二进制数据, 然后为每个Unit, 在某个文件中, 分配一段固定的空间, 用来存放它.
于是, 当你需要Unit的信息是, 你从该文件的固定位置读入.
当修改Unit的信息后, 你再将它写到那个固定位置.
如此一来, 数据就被存放于磁盘上了.
实现
这里为B+树提供一种最简单的思路.
首先将索引数据和实际数据分别存放于两份文件, 称之为IndexFile, DB.
B+树有一个BALANCE_NUMBER, 简称BN, 为定值, 那么一个B+树节点最多有2*BN个(key, value)的键值对.
我们将key固定为uint64, value固定为uint64类型.
那么一个B+树节点最多占用(8+8)*2*BN这么多byte, 将其表示为MAX_BYTES.
于是, 就可以这样来编码B+树了.
规定根节点在IndexFile的位移为0.
每当创建新的节点时, 在IndexFile尾部, 追加MAX_BYTES大小的空间.
然后将该空间在IndexFile的位移, 作为这个新节点的”位置”, 用该空间存放新节点.
于是, B+树内部节点的value就用来存放”对应子节点的位置”.
叶节点的value, 也被作为”位置”, 指向了该条记录在DB中的位移.
优化
上述实现会频繁的读写磁盘文件, 效率影响甚大.
为了解决这个问题, 可以加入一个模块, 这个模块分页管理IndexFile文件, 并对其进行必要的缓存, 以加快访问效率.
关于分页管理细节, 缓存算法, 不展开说了.
单事务, 单线程, 可靠的磁盘数据库
首先需要了解事务的基本概念, 参考<<数据库系统概念>>.
事务有ACID的性质, 由于现在是单线程版本, 所以不考虑其隔离性(I).
对于ACD这几个性质, 通常配合一定的”日志机制”完成.
于是需要去了解常见的”日志机制”.
这里推荐<<数据库系统概念>>日志恢复的那几章节.
实现
有了”日志机制”, 具体实现的时候还要考虑一些更加细节的东西.
这里是Sqlite的一篇官文, 描述了一些错误会怎么发生, 应该对操作系统做什么样的假设.
不必了解该文档每个细节, 但是可以扩展下思路: How To Corrupt An SQLite Database File
这里是Sqlite官方介绍怎么实现原子性的文档: Atomic Commit In SQLite
同样不需要了解每个细节, 可以扩展下思路.
个人总结
通常, 利用设计好的日志机制来保证事务的ACD性质.
然后利用对操作系统的一些假设, 来保证关键信息的原子性修改, 如数据库的”Boot”信息等.
如在我自己的实现中, 我就假设了操作系统的”rename