Myisam是最早实现的Mysql数据库引擎,也是人们心中的性能最好的引擎(虽然不是功能最强的,没办法,现实往往要求性能和功能做权衡)。这里选择 分析它,主要原因是其实现还算比较简单明了,而且最近我对数据文件的格式比较感兴趣,特别是变长数据的处理。要注意的是本文不会介绍myisam的索引文 件格式。
而对于MYD中的每个record,可以是fixed,dynamic以及packed三种类型之一。fixed表示record的大小是固定的,没有VARCHAR, blob之类的东东。dynamic则刚好相反,有变长数据类型。packed类型是通过
处理过的record。参见:http://dev.mysql.com/doc/refman/5.1/en/myisam-table-formats.html。
746 void mi_setup_functions(register MYISAM_SHARE *share)
747 {
....
759 else if (share->options & HA_OPTION_PACK_RECORD)
760 {
761 share->read_record=_mi_read_dynamic_record;
762 share->read_rnd=_mi_read_rnd_dynamic_record;
763 share->delete_record=_mi_delete_dynamic_record;
764 share->compare_record=_mi_cmp_dynamic_record;
765 share->compare_unique=_mi_cmp_dynamic_unique;
766 share->calc_checksum= mi_checksum;
767
768 /* add bits used to pack data to pack_reclength for faster allocation */
769 share->base.pack_reclength+= share->base.pack_bits;
770 if (share->base.blobs)
771 {
772 share->update_record=_mi_update_blob_record;
773 share->write_record=_mi_write_blob_record;
774 }
775 else
776 {
777 share->write_record=_mi_write_dynamic_record;
778 share->update_record=_mi_update_dynamic_record;
779 }
780 }
...
这是针对pack类型的处理函数设置。设置了 share结构中的一堆函数接口。顺便说一句,这种方式是C语言编程中常用的实现”多态“的办法:申明函数接口,动态设置接口实现,思想上和C++的动态 绑定是一致的。这段代码对于dynamic类型的表的record处理函数做了设置。比较有趣的是HA_OPTION_PACK_RECORD用来指定 dynamic类型。
看到这些函数名大家可以猜想出他们都是干嘛的,下面主要看看fixed类型和dynamic类型的具体处理。
Dynamic类型是相对于fixed的类型而言,这种类型可以容忍变长数据类型的存在。随之而来的是更复杂的数据文件的操作。Dynamic类型中被删 除的数据块也不是马上被释放,也被链表连起来。下次要写入新数据的时候,还是优先从这个链表中找。不同于fixed类型的处理在于新来的数据和链表中的空 间的大小可能不一样。如果新数据大了,就会找好几个空余空间,将数据分散于多个数据块中,如果新数据小了,则会将空余数据块分成两个,一个写入新数据,一 个还是放在空余链表中供后来者使用。
320 static int write_dynamic_record(MI_INFO *info, const uchar *record,
321 ulong reclength)
322 {
检查是否有足够的空间来存放新数据,空间满了返回错误。
351
352 do
353 {
// 找一个可以写入数据的地方。注意这里是在一个循环里面,也就是说每次找到的
// 空间不一定能够写入整个数据,只能写入部分的话,剩下的还要继续找地方写。
354 if (_mi_find_writepos(info,reclength,&filepos,&length))
355 goto err;
// 写入能够放入找到的空间的数据。
356 if (_mi_write_part_record(info,filepos,length,
357 (info->append_insert_at_end ?
358 HA_OFFSET_ERROR : info->s->state.dellink),
359 (uchar**) &record,&reclength,&flag))
360 goto err;
361 } while (reclength);
...
}
其中的循环说明了一切,很有可能一个数据会被分成几块儿,写到不同的地方,但是他们合起来才构成了整个数据。
再看_mi_find_writepos。
371 static int _mi_find_writepos(MI_INFO *info,
372 ulong reclength, /* record length */
373 my_off_t *filepos, /* Return file pos */
374 ulong *length) /* length of block at filepos */
375 {
376 MI_BLOCK_INFO block_info;
...
// 先检查dellink中是否有空余的空间。
380 if (info->s->state.dellink != HA_OFFSET_ERROR &&
381 !info->append_insert_at_end)
382 {
383 /* Deleted blocks exists; Get last used block */
存在空余空间,那就把链表中的头找出来,把其中的空间用来写入新数据。
将这块空间的描述返回给调用者。
....
398 }
399 else
400 {
401 /* No deleted blocks; Allocate a new block */
没有已删除的空间,那就在数据文件的最后分配空间,并返回给调用者。
421 }
...
}
如果有已删除的空间的话,那就直接把链表头描述的 空间返回。这个算法很简单,但是我觉得这样简单的算法可能会赵成一些问题,比如存储的碎片化,一块儿大空间被切的越来越小,到后来写入一个数据要使用好几 个空间。这些问题在操作系统的内存管理中也同样存在,所以产生了大量的内存管理算法,这里也应该可以借用吧。
具体的写入是在_mi_write_part_record中完成的。这个函数比较长,我就直接简写如下了。
int _mi_write_part_record(MI_INFO *info,
my_off_t filepos, /* points at empty block */
ulong length, /* length of block */
my_off_t next_filepos,/* Next empty block */
uchar **record, /* pointer to record ptr */
ulong *reclength, /* length of *record */
int *flag) /* *flag == 0 if header */
{
如果给出的空间空间大于数据长度的话,计算填完数据后剩余的空间。
如果空间刚好,准备一些元数据。
如果空间太小,则找到下一个写入空间的位置(要么是下一个dellink,要么是文件末尾),并准备这些元数据。如果是第一部分的数据的话,要写入更多的信息。
如果空间太大,有剩余空间的话,先看这个空间能否与和下一个空闲空间连接起来形成一个大空间,如果能的话就合并。将其相关的元数据,比如空间的位置,大小之类的,准备好。
开始写数据罗,如果启用了写缓冲,则写入缓冲,否则写入找出来的空间。
更新dellink的相关信息。
}
逻辑很清楚,主要是要处理空间过大或者过小带来的复杂性。
好了,到了这里大部分的处理都很清楚了,还是很直 接的。剩下的就是在删除一个数据的时候,将其所占的空间放到dellink中,要注意的是,如果其数据块可以和dellink中的其他数据块合并,合并操 作也是在删除数据的操作中调用的,而且合并出来的数据块还可能和其他数据块继续合并。有兴趣的自己看看delete_dynamic_record吧,我 就不写了。