tokyo cabinet源码分析-mdb设计和实现

  mdb - memory db

mdb是cabinet的一种数据组织方式,其他还有hdb(hash)、bdb(btree)等,详见"tokyo cabinet源码分析"。

由名字可知,mdb使用纯内存(不一定,见下面),速度最快。它是后面其他较高级的数据组织方式的基础,hdb、bdb的read cache都是直接使用mdb实现。而且mdb的实现最简单,所以先从mdb分析。

先贴出mdb结构图片:

tokyo cabinet源码分析-mdb设计和实现_第1张图片

这张是mdb的结构图,它表达了mdb数据组织和访问的方式:

1、最上层是maps数组,长度固定且有限(8个)。对key 的访问先hash到其中一个map;

2、每个map由buckets数组组成(较大,由参数决 定)。key经过第二次hash(不同于第一个的hash)到其中一个bucket;

3、hash到同一bucket的records用二叉搜索树 组织(查找效率高);

4、对key的查找在二叉搜索树中进行

这就是mdb的大体结构。

----------------------------------

接下来一层描述细致一些的方面:访问流程、多线程读写的同步、锁的粒度,等。

关键的数据结构。别被吓跑~ 很简单的:

typedef struct {

  void **mmtxs; /* pthread_rwlock_t */

  void *imtx; /* pthread_mutex_t */

  TCMAP **maps;

  int iter;

} TCMDB;

@mmtxs : 读写锁。用于锁maps,所以其个数和map个数相同(8个);

@imtx : iterator互斥锁。用于支持游标-遍历

@maps :即图片中的maps,结构定义见下。

typedef struct {                         /* type of structure for a map */

  TCMAPREC **buckets;                    /* bucket array */

  TCMAPREC *first;                       /* pointer to the first element */

  TCMAPREC *last;                        /* pointer to the last element */

  TCMAPREC *cur;                         /* pointer to the current element */

  uint32_t bnum;                         /* number of buckets */

  uint64_t rnum;                         /* number of records */

  uint64_t msiz;                         /* total size of records */

} TCMAP;

@buckets : 图中的buckets,指向用二叉搜索树组织起来的records,结构定义见下。

@first

@last

@cur : 这三个指针分别指向该map中第一个、最后一个和当前一个record。显然是用于支持游标-遍历的实现。

@bnum

@rnum

@msiz : 这三个是统计信息:buckets num, records num, key value total size

typedef struct _TCMAPREC {               /* type of structure for an element of a map */

  int ksiz;                              /* size of the region of the key */

  int vsiz;                              /* size of the region of the value */

  unsigned int hash;                     /* second hash value */

  struct _TCMAPREC *left;                /* pointer to the left child */

  struct _TCMAPREC *right;               /* pointer to the right child */

  struct _TCMAPREC *prev;                /* pointer to the previous element */ /* for listing */

  struct _TCMAPREC *next;                /* pointer to the next element */ /* queue */

} TCMAPREC;

@ksiz

@vsiz : key,value的长度

@hash : key的hash值。用于record沿二叉树查找时比较。(当然hash值相同并不能证明一定是相同key,但不同的话肯定是不同key,详见下)

@left

@right : 用于组织树。left child, right child

@prev

@next : 双向链表,将该map所有records串连起来。不用说,肯定是用来实现游标-遍历

解释:

在这个record结构体的定义里并没发现key,value 的定义,是因为它是一个变长结构(C编程技巧)--因为key、value长度事先并不知,运行时实际插入record时才能知道,所以采用了这种结构。 优缺点:优点是灵活和空间节约;缺点是需要动态分配,拖累性能--你也可以使用内存池预分配自己做管理,但对这种长度完全不定的串的管理似乎也不是件容易 的事。具体我还没细想。不管怎样,cabinet在此是完全使用动态分配。

关于C的变长结构小技巧,就是分配 时:malloc(sizeof(TCMAPREC) + ksiz + vsiz),这样就分配了足够的空间,结构体外多余的空间是紧挨着该结构体的(一次分配的),利用成员ksiz、vsiz就能对它们进行访问。一种常用技 巧是在结构尾部定义key[0]这种方式访问,另一种方式是直白的record指针+sizeof(TCMAPREC)来访问,据此你甚至可以用宏定义来 虚拟出key指针,小技巧,不多说。

另外实际的存储中key、value间是有做对齐的 (padding),但对描述mdb设计并没影响。

-------------------------------

下面描述一些函数的实现流程:

初始化一个mdb(tcmdbnew):

1、参数处理:

参数bnum: buckets num。调用者不指定的话使用默认值:65536

程序对传入的bnum做进一步处理:

bnum = bnum / 8 + 17 ;

也就是:传入参数过小的话就使用17(质数,提高hash效 果),大的话就用maps数平均一下(8个map)

2、锁的初始化等;

3、分别对8个maps的buckets进行存储分配:

如果新bnum > 32768 , 使用mmap(所以前面说不一定纯内存);

否则 ,使用malloc

解释:

如果你用参数指定过多buckets的话,就会使用mmap。 实际使用中一般就是用默认值,根据上面的等式简单计算知道它用malloc,即纯内存;

关心多少的buckets临界值会让mdb由malloc转为 使用mmap的话,可以自己算一下~

---------------------------

record插入(tcmdbput):

TCMDBHASH得到map下标;
wrlock该map;
TCMAPHASH1得到bucket下标;
TCMAPHASH2得到key的hash值,用于下面的比较;
key的二叉搜索树查找:
如果key的hash值大于当前record的hash值,goto left
 child
如果大于,goto right
 child
如果等于,strcmp:
如果key小于
record的key,goto left  child
如果大于,goto right
 child
如果等于,找到。写入新的value。(比旧value大就realloc)
没找到key,此时key在二叉树应插入的位置。分配空间填入key、value并把此record插入树
unlock map

解释:

根据上面的图片应该能很好理解插入的流程,它通过不同hash 函数一级级往下找到bucket,再在二叉树中用hash值对key进行查找。因为相同hash值不代表key一定相同,所以找到相等hash值时还需用 strcmp进行串比较。但显然它先用hash比较排除了大量不符的key,大大提高了性能。

本节前面提到mdb的内存分配完全采用动态分配,并认为它会影 响到性能。这里有这样一个处理:put时如果旧value空间不足以放下新value,当然就realloc;如果足够,则是直接使用旧的空间,避免了内 存的分配。这样程序运行一段时间后需要做动态内存分配的次数会越来越少(因为只增加不减少)。这里有这样一个问题:如果新value所需空间远小于旧 value空间(比如小于1/2),是否需要free掉旧的大空间然后分配一个小空间给新value呢?我觉得取决于应用,作为一个通用的系统,我觉得它 应该在空间比到某个系数(比如1/2)时做重分配。但mdb没这么做。

这里有一个问题就是锁的粒度,在后面函数流程描述完了再细说;

这几个hash函数的实现代码放在本节的最后面,它们属于细枝 末节。

---------------------------

record删除(tcmdbout):

record查找逻辑和put一模一样,hash到map下标,锁住map,hash到bucket下标,计算
key的hash值,沿二叉搜索树往下查找:
没找到
要删除的key不存在,直接返回
找到
树结构更新:该record只有左子树或只有右子树,则把相应的子树接上去;都有的话就用左子树接上去,并把右子树接到左子树

最右边record的右边...(二叉搜索树的基本操作,不细说了,详细资料可
以搜索得到)
释放record

---------------------------

r ecord查找(tcmdbget):

record查找逻辑和put一样,唯一的区别是对map加读 锁(很显然)

返回value

-------------

关于锁粒度:

我觉得mdb的锁粒度太粗(才8个锁),做插入或删除的时候直 接写锁住整个map(全部数据的1/8左右),其他需要访问此map的操作全部得串行、阻塞。(很怀疑我是不是代码看错了~~ )

后面分析hdb实现可以看到:hdb没有maps这一层,整个 就一个大的buckets,锁粒度细到了接近每个record对应一个锁(行级锁)。那是一个比较好(也比较好理解)的设计;

我对mdb这点设计不太能理解,过几天会再细看下并调参数测一 下。

-->

之后又想了下:

要考虑到mdb在tc中的作用主要是作为hdb、bdb的 read cache使用的,所以:

1、它不应该使用大量读写锁,因为hdb、bdb都使用了大 量的读写锁(默认十几万个)。原因从浅层分析是会占用过多资源,进而影响性能。(知道深层更细致原因的可以补充);

2、作为hdb等的read cache,对它的主要操作是get,锁粒度的粗细对rwlock的read并没影响。mdb也有delete操作,一个例子是对hdb做put时,需要 先从其mdb的cache里delete掉那条record(详细可参见对hdb的分析)。

所以,这个设计和参数选择应该也算是一种折中。这种参数应该 算是"实验数据",可能做了大量测试后才能找到一个好的平衡点。

---------------

游标-遍历 实现

--------------

从最上面结构体定义和说明可以看到,每个map都有三个指针:first、last、cur分别用于指向该map第一个、最后一个和当前一个 record。record结构内部的prev、next则将所有的records串连起来。

就是它们为游标-遍历的实现提供支持。

游标初始化(tcmdbiterinit):

互斥锁锁住mdb->imtx

初始化各map的cur指向其first

解锁

取下一条record的key(tcmdbiternext):

互斥锁锁住mdb->imtx

写锁锁住第mdb->iter个map

返回map的当前游标cur指向record的key和 ksiz

解锁

解释:

iternext函数只是返回key和ksiz,并未修改它, 为何要对map加写锁,而非读锁呢?是因为每次读取后需要修改map的当前游标cur,让它指向next,显然这是不允许多个线程同时执行的;

iterator使用互斥锁而不是读写锁也是这个原因:每次操 作都有数据的更新。

----------------------

最下一层:一些资料

上面描述中提及的几个hash函数(第一个是hash界大名鼎鼎的times33~~,接着的几个是变种吧):

#define TCMDBHASH(TC_res, TC_kbuf, TC_ksiz) /

  do { /

    const unsigned char *_TC_p = (const unsigned char *)(TC_kbuf) + TC_ksiz - 1; /

    int _TC_ksiz = TC_ksiz; /

    for((TC_res) = 0x20071123; _TC_ksiz--;){ /

      (TC_res) = (TC_res) * 33 + *(_TC_p)--; /

    } /

    (TC_res) &= TCMDBMNUM - 1; /

  } while(false)

#define TCMAPHASH1(TC_res, TC_kbuf, TC_ksiz) /

  do { /

    const unsigned char *_TC_p = (const unsigned char *)(TC_kbuf); /

    int _TC_ksiz = TC_ksiz; /

    for((TC_res) = 19780211; _TC_ksiz--;){ /

      (TC_res) = (TC_res) * 37 + *(_TC_p)++; /

    } /

  } while(false)

#define TCMAPHASH2(TC_res, TC_kbuf, TC_ksiz) /

  do { /

    const unsigned char *_TC_p = (const unsigned char *)(TC_kbuf) + TC_ksiz - 1; /

    int _TC_ksiz = TC_ksiz; /

    for((TC_res) = 0x13579bdf; _TC_ksiz--;){ /

      (TC_res) = (TC_res) * 31 + *(_TC_p)--; /

    } /

  } while(false)

----------

补充说明

----------

关于一些函数的具体实现代码要不要列出来

有些人对其设计有兴趣的话会想看看它的具体实现代码是怎样的, 我也不太清楚要不要列出来。因为只是简单的罗列,而且会让文章显得很长很乱。所以想研究具体实现细节的请自己直接看源代码。

你可能感兴趣的:(tokyo cabinet源码分析-mdb设计和实现)