像 InnoDB 这种底层存储结构,在代码层面是如何实现的呢

2021-01-05 14:05:46 +08:00
 zxCoder

比如很多书上以及文档都会讲这个存储引擎的存储结构,比如记录的结构,页的结构,那么这些东西映射到代码层面是怎么实现的呢?

比如一个页,里面有很多个记录,对应到内存和硬盘里,是一堆字节,那我们的程序代码要处理的时候,是需要讲这块的内容读到内存,然后类似反序列化转化为我们代码中的一些结构体比如 Page,Record 对吗?然后操作的逻辑就是对这些对象操作,最后再序列化刷回磁盘这样子吗?

按书上的做法,页面的 UserRecords 里的记录删除后只是会先打一个标记,那么为什么不这样做,比如这个页面的记录在我们代码中体现的就是一个 LinkList,我们可以直接删掉要删掉的记录,再序列化写回磁盘里。

这块想不太清楚,希望各位大佬指点一下。

3572 次点击
所在节点    数据库
25 条回复
wps353
2021-01-05 14:39:16 +08:00
1 、因为数据库本身是一个很复杂的东西。要考虑很多层面上的东西,比如事务隔离级别,MVCC,锁等等相关的情况。暴露外面的接口看上去都是 CRUD 的简单接口,但事实上内部实现极其复杂,要考虑很多东西。
2 、「按书上的做法,页面的 UserRecords 里的记录删除后只是会先打一个标记,那么为什么不这样做,比如这个页面的记录在我们代码中体现的就是一个 LinkList,我们可以直接删掉要删掉的记录,再序列化写回磁盘里。」这里为什么不能直接删除了?打个比方,在 RR 隔离级别下,事务 A select id=1,事务 B 然后 delete=1,commit;然后事务 A 在 select id=1,你会发现事务 B 的删除根本对事务 A 没什么影响。所以这里要考虑数据在不同的隔离级别的可见性。
dynastysea
2021-01-05 14:41:08 +08:00
没有什么序列化的操作,都是直接修改内存,然后 buf 更新到磁盘
la2la
2021-01-05 14:45:36 +08:00
实现太复杂了,可以翻翻 MySQL 的源码。
zxCoder
2021-01-05 14:47:14 +08:00
@dynastysea 啊 这么硬核的吗
dynastysea
2021-01-05 14:52:16 +08:00
@zxCoder 一点也不硬核,可能你不是 C 程序员
zxCoder
2021-01-05 14:54:10 +08:00
@dynastysea 能举个例子吗 “直接修改内存”, 我修改数组某个值也算修改是直接修改内存吗
dynastysea
2021-01-05 15:01:25 +08:00
@zxCoder 就是这个意思,比如你说的 Page 结构体,你改了其中一个字段,它是内存的一个 buf,然后调用 write(fd, buf,len)更新到文件即可。
zxCoder
2021-01-05 15:08:56 +08:00
@dynastysea 可能我序列化的表达有的问题,我想说的是比如有一些标志位用一个 bit 来表达嘛,那代码里是否可以用一个 bool 来代替,然后当需要写入的时候再判断,比如某一个字段 15 个 bit,代码里是否能用一个 short 来代替,当需要写入的时候再把这个 short 的 15 个 bit 写入,

还是说只是用 byte 数组代替整个结构,然后直接操作每一位,比如我要读取 15bits 这个字段,我再去计算得到一个值?
newlife
2021-01-05 16:23:53 +08:00
可以看下 boltdb,项目很小,几千行把, 这个是 go 写的单文件的 k-v 数据库, 索引也用的 b+树。看完了你就可以回答你提出的问题了
zxCoder
2021-01-05 16:33:44 +08:00
@newlife 啊这
zxCoder
2021-01-05 16:35:38 +08:00
@newlife 我觉得还是先弄懂我的问题吧 这项目看起来跟我想的差的有点多,至少我看不太懂 page.go 里写的啥
extreme
2021-01-05 19:29:31 +08:00
如何实现,看你需求
例如你有 32 个 bool 值
假如你想省储存空间,当然可以用 bitmap,例如你用一个 int32 就能储存这 32 个 bool 值
当然你也可以直接选择使用 32 个 int8 去表示,操作起来方便,但是耗空间

如果要你储存十亿个 IPv4 地址,你打算咋存呢?
直接存 int32 你需要接近 4GB 储存空间,用 bitmap 只需要 120MB
neoblackcap
2021-01-05 20:03:35 +08:00
内存跟数据结构没有什么关系
当然你粗略地理解存储引擎在处理数据的时候是会序列化与反序列,这并没有什么问题。
像 C 那样能直接操作内存的语言,字节就是数据结构,取决你如何“解释”而已
xiangbohua
2021-01-05 20:04:41 +08:00
我感觉的吧,不考虑事务这些复杂的功能的话,其实 InnoDB 这种数据库引擎做的事情,也就是通过合理的算法将需要写入的数据写入合适的硬盘块。
Mohanson
2021-01-05 20:10:18 +08:00
500 lines 有个叫 dbdb 的项目, 用的 btree + 单文件, 入门推荐

http://www.aosabook.org/en/500L/dbdb-dog-bed-database.html
zxCoder
2021-01-05 21:35:31 +08:00
@extreme

比如我看 innodb 一个 page 里有多个记录,通过一个“下一个记录相对偏移量”,将所有记录连成一个单链表,

那比如我用 c 语言现在读取这个 page,也就是对应了一个字节数组对吧,比如我现在要遍历这个 page 查找某个记录,就是直接这样 bytes[i]到 bytes[i+next1]到 bytes[i+next1+next2]这样直接去操作对吧,比如遍历到 bytes[x],然后因为这个列是 32 位整数,就需要往后读 4 个字节到 bytes[x+3],然后转成一个整数,和我要查询的值比较判断。

也就是从始至终我都是直接在这个字节数组里操作的对吗

脑子有点乱表达有点乱,希望得到解答 orz
extreme
2021-01-05 23:28:28 +08:00
对的,把数据从硬盘读取到内存当中,有一个指针存了那份内存的起始地址。
那个指针在 C 语言里面一般是个“uint8 *”类型的指针,可以当作一个 uint8 数组来操作,也就是你 Java 里面的“字节数组”。
后面要读写那份在内存里面的数据,就通过 uint8 的指针去操作。

至于你说的往后读 4 个字节,转成一个整数,其实转换的只是类型,直接用一个 int32 类型的指针指向那个内存地址,就可以读出一个整数了,例如 bytes[x]是你说的一个 32 位的整数,假设它是无符号的,直接*(unit32 *) (bytes + x)就能把这个 uint32 读取出来,转的只是指针的类型,而不是值:
uint8_t *bytes = ...; // 指向“页”所在的内存地址
uint32_t x = ...; // 你那个 32 位整数的在这个页里面的偏移量
uint32_t *uint32_pointer = (uint32_t *) (bytes + x);
printf("%d\n", *uint32_pointer); // 输出那个整数的值

*uint32_pointer = 0x0A0B0C0D; // 更改整数的值
printf("%x, %x, %x, %x\n", bytes[x + 0], bytes[x + 1], bytes[x + 2], bytes[x + 3]); // 输出 d, c, b, a
extreme
2021-01-05 23:44:38 +08:00
@zxCoder 具体如何读写其内存中的数据,还得看那种类型的具体实现。
例如说 NULL 值其实 InnoDB 是通过 bitmap 储存的,就通过指针读取那一行用来记录 NULL 列的 bitmap,通过位运算进行 NULL 值的读写
反正,都是通过指针,在内存中对数据进行读写,也就是你所说的“在字节数组里操作”,涉及的只有指针类型的转换,一般不会发生值的转换
elgoogelgoog
2021-01-06 00:00:58 +08:00
carlclone
2021-01-06 01:15:02 +08:00
@wps353 不直接删除是为了减少碎片,后面重用,而不是什么事务隔离级别的问题,内存里也有这种策略,分页

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/741845

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX