由于项目里要用到mongodb,不看一下mongodb的存储原理,实在是对这个之前没了解过的数据库不太放心,所以查了些资料学习了下。下面基于WiredTiger引擎行存储做一些基本原理的说明,涉及的图片我就不自己画了,盗取下网上的图^_^。
首先是整体结构,WiredTiger 数据组织方式就是 in-memory page 加 block-extent:
内存page以B+树的结构组织,每个Btree节点为一个page,root page是btree的根节点,internal page是btree的中间索引节点,leaf page是真正存储数据的叶子节点;btree的数据以page为单位按需从磁盘加载或写入磁盘。
Wiredtiger采用Copy on write(快照)的方式管理修改操作(insert、update、delete),修改操作会先缓存在cache里,checkpoint时,会产生一个新的root page,此时对页面的修改会新分配索引page和数据page。原来的B+树结构实际成为一个快照,由后台线程执行checkpoint:
Page内部增加了多个跳表用于提升读写性能,通过一个实例来说明,假如一个 page 存储了一个 [0,100] 的 key 范围,磁盘上原来存储的行 key=2, 10 ,20, 30 , 50, 80, 90,他们的值分别是value = 102, 110, 120, 130, 150, 180, 190。在 page 数据从磁盘读到内存后,分别对 key=20 的 value 进行了两次修改,两次修改的值是分别 402,502。对 key = 20 ,50 的 value 做了一次修改,修改后的 value = 122, 155,后有分配 insert 了新的 key = 3,5, 41, 99,value = 203,205,241,299。那么在内存中的 page 就是如下图组织数据的:
row_array:磁盘上原有 row 的位置索引数组,主要用于页内检索。
row_insert_array:一个增加 kv(insert k/v)的跳表对象数组。
row_update_array:一个在 row 基础上做更新(update k/v)的 value mvcc list对象素组。
page_disk: 从磁盘上读取到的 extent 数据缓冲区,包含一个 page_header 和一个数据存储缓冲区(disk data)。
page disk data:存储在磁盘上的 page k/v cell 行集合数据。
Page页内检索时,通过row_array/insert_array/update_array 数组一一对应的查找。
索引说明
往某各个集合插入多个文档后,每个文档在经过底层的存储引擎持久化后,会有一个位置信息,通过这个位置信息,就能从存储引擎里读出该文档。在wiredtiger存储引擎(一个KV存储引擎)里,位置信息是wiredtiger在存储文档时生成的一个key,通过这个key能访问到对应的文档;为方便介绍,统一用pos(position的缩写)来代表位置信息。
位置信息文档
pos1 {“name” : “jack”, “age” : 19 }
pos2 {“name” : “rose”, “age” : 20 }
pos3 {“name” : “jack”, “age” : 18 }
pos4 {“name” : “tony”, “age” : 21}
pos5 {“name” : “adam”, “age” : 18}
假设现在有个查询db.person.find( {age: 18} ), 查询所有年龄为18岁的人,这时需要遍历所有的文档(『全表扫描』),根据位置信息读出文档,对比age字段是否为18。当然如果只有4个文档,全表扫描的开销并不大,但如果集合文档数量到百万、甚至千万上亿的时候,对集合进行全表扫描开销是非常大的,一个查询耗费数十秒甚至几分钟都有可能。
如果想加速db.person.find( {age: 18} ),就可以考虑对person表的age字段建立索引。
db.person.createIndex( {age: 1} )// 按age字段创建升序索引
建立索引后,MongoDB会额外存储一份按age字段升序排序的索引数据,索引结构类似如下,索引通常采用类似btree的结构持久化存储,以保证从索引里快速(O(logN)的时间复杂度)找出某个age值对应的位置信息,然后根据位置信息就能读取出对应的文档。
age位置信息
18 pos3
18 pos5
19 pos1
20 pos2
21 pos4
可以看到,WiredTiger索引的存储结构原理和Innodb等关系型数据库引擎没太大差别。