Berkeley DB的动态散列技术

 

本文最初贴在我在百度空间的blog(http://hi.baidu.com/dazhao_dbblog),现在把它也发布到这里。

 

 

一个Berkeley DB数据库(也就是关系数据库中的一个表)存储的数据是key/data pair(键/值)对的集合,这些key/data pairs原本可以是任何结构、模式和意义的数据,Berkeley DB不理会它们的结构和意义,只把他们当作字节串来存取;但是从应用程序设计的角度看,一个数据库中最好是保存一类数据,比如在同一个Berkeley DB数据库中保存所有员工的信息,对于每一个key/data pair,以工号为key,以一个员工的全部信息为data。要存储部门信息时,把它保存到另一个Berkeley DB数据库中。

在Berkeley DB内部,保存key/data pair的方法有四种,在Berkeley DB的术语中,每种都叫做一个access method (访问方法)。这四种access method分别是btree, hash, recno, queue. btree把数据集(key/data pairs)组织成一个b+树,这样它很适合范围查找,插入删除速度也很快,是默认应该选择的方法;hash方法通常具有和btree接近的性能,适合精确查找,而对范围查找来说,它性能自然较差。recno和queue方法把数据集组织成一个数组,不需要用户提供key,key是内部维护的是递增的整数,可以当作下标来访问到对应的数据。其中recno支持变长的数据项,queue要求数据项定长。根据数据集特性的不同(全部数据量大小、每个key/data pair的大小,key/data pair是否定长)以及数据访问方式的不同(范围查找、精确查找、按照编号访问等),用户可以选择不同的access method. 本文要介绍的是hash access method的动态散列技术。

Berkeley DB的hash access method(HASH AM)有以下一些需求
1. 适应很小的数据量和很大的数据量,按需分配空间
2. 要有较快的精确查找、插入速度
3. 每一个key/data pair中,能够适应任意大小的key或者data
4. 元数据占用空间小

所以它所使用的动态散列技术正是用于满足这些需求的。Berkeley DB的hash方法用用户提供的key和用户可配置的hash函数计算一个哈希码H, 然后用H通过动态散列技术得到这个key所在的桶,然后去桶中找这个key或者将新的key/data pair放入这个桶中。它用若干个页组成一个桶,理想情况下是每个桶一页。在每个桶中,key的排列是页内有序,页间无序的---在插入新的 key/data pair到一个页中的时候,我们保持这个页内的Key是有序的,这样在这个页中查找一个key的时候,就可以使用二分法迅速找到目标key/data pair.

起初,在hash 数据库中只有两个桶,每个桶各一页,桶的编号分别是0和1,同时我们有两个掩码HiMASK和LoMASK, 他们起初分别为0x00000001和0x00000000 。 当我们提供一个key给HASH AM时,这个key首先被送入hash 函数(可配置)得到一个hash code H,H是一个32位整数。我们计算桶号B = HiMASK & H ---如果B是0(1),那么我们去0(1)号桶查找或者插入所要的key/data pair. 0号和1号桶构成了第1代的桶,这里的代/generation 是若干个桶的一个集合,这些桶的第一页总是连续排放的。当0号桶满了的时候,我们就需要产生第2代的桶并且分裂满了的0号桶。方法是:首先改变HiMASK和 LoMASK :
HiMASK = 2 * HiMASK + 1, LoMASK = 2 * LoMASK + 1 (1)
然后,分配2号桶(1页),同时记录下来当前最大的桶号MAX_BUCKET 是2. 然后,分裂0号桶,把它一半的key/data pairs放入2号桶,通过这样的方式:对0号桶的每一个key做如下计算:
H = HASH(key); (2)
B = HiMASK & H > MAX_BUCKET ? LoMASK & H : HiMASK & H; (3) (这个计算桶号的方法是整个Berkeley DB的动态hash方法中通用的)
所以B可能的值就是0或者2(因为H的最低位必然是0,次低位可能是0或者1). 当计算出key仍旧为0的,保持它不动,还在0号桶里面;key为2的,放入2号桶。 这样0号桶的数据少了一半。当某个key计算出H的最低2位均为1时,B仍然是1,所以还要放入1号桶,直到1号桶满了,也要进行分裂,这时,最高的代仍然是第二代,所以HiMASK和 LoMASK不需要改变,但是MAX_BUCKET变为3. 所以对1号桶的数据计算新的B值时,遇到1的仍然留在1号桶,是3的,放入3号桶。

有人可能会问,如果首先满了的不是0号桶而是1号桶改怎么办呢?答案是这时候1号桶不能分裂,我们只好增加一个页给1号桶,把新来的key/data pairs放入新的页中。这样1号桶就有2页了。直到0号桶满了,分裂了,1号桶才可以分裂。所以如果hash函数性能不好的话,很可能有一些桶有很多页,另一些桶很空,这样的hash函数会影响数据库的性能,用户需要根据自己的数据集的特性,注册一个优良的hash函数。为什么一定要按照桶号递增顺序分裂呢?假设任何一页满了,我们都可以分裂,并且我们当前最高代是1代。这时1号桶先于0号桶满了,我们改变HiMASK, LoMASK, 并且设置MAX_BUCKET = 3,这是因为最低位是1的,如果取出最低2位的话,只可能是1或者3. 根据公式2和3计算桶号B为1的,留在1号桶,为3的,放入新分配的3号桶。那么当一个key计算出为2的时候,我们会发现,2号桶里面没有这个key,算法就出错了。也就是说,当不从0号桶开始连续递增地分裂桶的话,被分裂桶之前的那些桶中的key在查找的时候按照这个算法可能找不到。当然,我们可以在分裂1号桶的时候同时把它之前的所有桶---此例中只有0号桶---分裂,这样算法不就依然正确吗?但问题是,当有很多代的桶的时候,我们可能不得不一次分裂很多个桶,而分裂一个桶是很昂贵的操作---桶中每个key都要计算H和B,并且一半的 key/data pairs要改变位置。在hash函数不均匀的时候,还会造成很大的空间浪费,并且会占满数据库缓存从而影响性能。

通过上面的描述,我们不难看出,第一代桶有 0, 1号桶,第二代桶是由前面所有各代每个桶各分裂一次得到的,所以是2,3号桶;第三代就是4 (由0桶分裂), 5(由1桶分裂), 6(由2桶分裂), 7(由3桶分裂)。 第N代就是2^(n - 1) 个桶,编号分别是 2^(n-1), 2^(n-1) + 1, .... 2^n -1 (n > 1). 第一代是特例,不使用本公式。所以我们也可以很容易的到前N代一共有多少个桶---2^n . 这就是为什么我们不能不连续分裂---当n是5的时候,我们可能一次就要分裂32个桶,如果最后一个桶(31号)最先满的话!

数据库的存储单位是页,当我们知道桶号之后,还要知道这个桶的第一个页是哪一个(其余的页可以从第一个页存储的页号指针得知),所以在hash数据库的元数据页中我们存放了一个代号--页号表:保存了这样的映射关系: 第i代----本代最低桶号的第一页号。注意,同一代各个桶的首页号码是连续且递增,并且我们知道每代桶分别有多少个桶,所以有了本代最低桶号的第一页号,我们就可以知道本代各个桶的第一页号,进而得知本代各个桶的全部页。有人可能想,既然如此,那么我们把各代桶的首页都连续递增存放的话,我们只需要知道0号桶的首页页号即可,问题是这个条件太苛刻了,这样虽然得到了一些效率的优势,但是,当一个桶需要增加一页的时候,我们没办法分配任何一页给它,因为任何一页必然将属于未来某代桶;而预留较低页号也不合适---将来空间可能不足,或者大量空间浪费。所以我们只要求同代桶的首页号连续递增,并且在两代之间预留一些页用于分配给该代需要增加页的桶,比如:第一代桶起初在 1,2页,第二代起初在第6,7页,而3~5页留着给第一代的桶溢出时使用,好处是:同一个桶的各个页存放在磁盘上连续的一片区域,磁盘的读取速度较快。(数据库文件按页来增长,Berkeley DB按页管理数据库中的数据。从文件第一个字节开始,页号从0开始递增,每页N个字节。N可配置,默认是操作系统磁盘block的大小)

那么,如果一个数据项大于一页(比如一张2M的图片,或者一部500M的电影)改怎么存储呢? 这个问题我们在下一篇中探讨。

上述算法的好处是,hash access method的四个需求都达到了,首先桶是按需分配的,不需要预先占据很多空间;在插入、查找的时候,最优情况下可以直接得到目标页,平均来说读取磁盘次数不会多于btree。在btree算法中,当数据量极大时,b+树内结点占据的空间也会很大,可能超过机器的物理内存,会导致数据库运行极其缓慢,内结点存储的不是用户的数据,我们叫做元数据。而hash方法中只有一个元数据页,所以在数据量极大的时候,hash很可能是唯一合理的选择。

缺点和可以改进的地方是:首先,桶的分裂带来了比较昂贵的开销,当目前有N代桶时候,已经分裂了至多2^N个桶,为了分裂一个桶,我们对同一个 key计算了最多N次hash值H和桶号B。其次,桶内的key不是完全有序的,只是页内有序页间无序,这样查找速度并不是最优的。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值