Berkeley DB初探

前言

最近一個一個相似文章搜索功能時,要保存文章庫的向量矩陣,由於矩陣太大無法一次加載到內存中,加上項目屬於輕量級。總結了一下需求其實很簡單,就是從一個非常大的矩陣中(無法一次性加載到內存),每次會隨機通過文章編號得到其tfidf向量(矩陣的一行數據)。發現嵌入式數據庫Berkeley DB可以很好的滿足我的需求。只要把文章編號看成key,tfidf向量看成data,就可以利用Berkeley DB實現我的需求。

UNIX/LINUX平臺下的數據庫種類非常多,參考資料1中列舉了其中的大部分。通常,我們在設計UNIX/LINUX平臺下的應用軟件時,如果數據種類繁多,數據與數據之間關係比較複雜,就會選用一些大型的企業級數據庫系統,如DB2,ORACLE、SYBASE等,如果軟件規模不大,就傾向選用如MYSQL、POSTGRESQL等中小型數據庫。例如使用PHP/PERL + MYSQL/POSTGRESQL設計網站基本上是一個很常規的做法。但是,當應用軟件管理的數據類型較少(特別注意:這並不是說需要管理的數據量小),數據管理本身不復雜,且對數據操作要求高效率,則由大名鼎鼎的Berkeley(美國加州大學伯克利分校)開發的 Berkeley DB可能是一個很明智的選擇。

DB綜述

DB最初開發的目的是以新的HASH訪問算法來代替舊的hsearch函數和大量的dbm實現(如AT&T的dbm,Berkeley的ndbm,GNU項目的gdbm),DB的第一個發行版在1991年出現,當時還包含了B+樹數據訪問算法。在1992年,BSD UNIX第4.4發行版中包含了DB1.85版。基本上認爲這是DB的第一個正式版。在1996年中期,Sleepycat軟件公司成立,提供對DB的商業支持。在這以後,DB得到了廣泛的應用,當前最新版本是4.3.27。

DB支持幾乎所有的現代操作系統,如LINUX、UNIX、WINDOWS等,也提供了豐富的應用程序接口,支持C、C++、JAVA、PERL、TCL、PYTHON、PHP等。DB的應用十分廣泛,在很多知名的軟件中都能看到其身影。例如參考資料2中作者談到利用DB在LINUX下實現內核級文件系統;參考資料3中通過實際測試數據說明DB提高了OPENLDAP的效率。LINUX下的軟件包管理器RPM也使用DB管理軟件包相關數據,可以使用命令file查看RPM數據目錄/var/lib/rpm下的文件,則有形式如下的輸出:

Dirnames: Berkeley DB (Btree, version 9, native byte-order) 
Filemd5s: Berkeley DB (Hash, version 8, native byte-order)

值得注意的是DB是嵌入式數據庫系統,而不是常見的關係/對象型數據庫,對SQL語言不支持,也不提供數據庫常見的高級功能,如存儲過程,觸發器等。

DB的設計思想

DB的設計思想是簡單、小巧、可靠、高性能。如果說一些主流數據庫系統是大而全的話,那麼DB就可稱爲小而精。DB提供了一系列應用程序接口(API),調用本身很簡單,應用程序和DB所提供的庫在一起編譯成爲可執行程序。這種方式從兩方面極大提高了DB的效率。第一:DB庫和應用程序運行在同一個地址空間,沒有客戶端程序和數據庫服務器之間昂貴的網絡通訊開銷,也沒有本地主機進程之間的通訊;第二:不需要對SQL代碼解碼,對數據的訪問直截了當。

DB對需要管理的數據看法很簡單,DB數據庫包含若干條記錄,每一個記錄由關鍵字和數據(KEY/VALUE)構成。數據可以是簡單的數據類型,也可以是複雜的數據類型,例如C語言中結構。DB對數據類型不做任何解釋, 完全由程序員自行處理,典型的C語言指針的"自由"風格。如果把記錄看成一個有n個字段的表,那麼第1個字段爲表的主鍵,第2--n個字段對應了其它數據。DB應用程序通常使用多個DB數據庫,從某種意義上看,也就是關係數據庫中的多個表。DB庫非常緊湊,不超過500K,但可以管理大至256T的數據量。

DB的設計充分體現了UNIX的基於工具的哲學,即若干簡單工具的組合可以實現強大的功能。DB的每一個基礎功能模塊都被設計爲獨立的,也即意味着其使用領域並不侷限於DB本身。例如加鎖子系統可以用於非DB應用程序的通用操作,內存共享緩衝池子系統可以用於在內存中基於頁面的文件緩衝。

DB核心數據結構

數據庫句柄結構DB:包含了若干描述數據庫屬性的參數,如數據庫訪問方法類型、邏輯頁面大小、數據庫名稱等;同時,DB結構中包含了大量的數據庫處理函數指針,大多數形式爲 (*dosomething)(DB *, arg1, arg2, …)。其中最重要的有open,close,put,get等函數。

數據庫記錄結構DBT:DB中的記錄由關鍵字和數據構成,關鍵字和數據都用結構DBT表示。實際上完全可以把關鍵字看成特殊的數據。結構中最重要的兩個字段是 void * data和u_int32_t size,分別對應數據本身和數據的長度。

數據庫遊標結構DBC:遊標(cursor)是數據庫應用中常見概念,其本質上就是一個關於特定記錄的遍歷器。注意到DB支持多重記錄(duplicate records),即多條記錄有相同關鍵字,在對多重記錄的處理中,使用遊標是最容易的方式。

數據庫環境句柄結構DB_ENV:環境在DB中屬於高級特性,本質上看,環境是多個數據庫的包裝器。當一個或多個數據庫在環境中打開後,環境可以爲這些數據庫提供多種子系統服務,例如多線/進程處理支持、事務處理支持、高性能支持、日誌恢復支持等。

DB中核心數據結構在使用前都要初始化,隨後可以調用結構中的函數(指針)完成各種操作,最後必須關閉數據結構。從設計思想的層面上看,這種設計方法是利用面向過程語言實現面對對象編程的一個典範。

DB數據訪問算法

在數據庫領域中,數據訪問算法對應了數據在硬盤上的存儲格式和操作方法。在編寫應用程序時,選擇合適的算法可能會在運算速度上提高1個甚至多個數量級。大多數數據庫都選用B+樹算法,DB也不例外,同時還支持HASH算法、Recno算法和Queue算法。接下來,我們將討論這些算法的特點以及如何根據需要存儲數據的特點進行選擇。

B+樹算法:B+樹是一個平衡樹,關鍵字有序存儲,並且其結構能隨數據的插入和刪除進行動態調整。爲了代碼的簡單,DB沒有實現對關鍵字的前綴碼壓縮。B+樹支持對數據查詢、插入、刪除的常數級速度。關鍵字可以爲任意的數據結構。

HASH算法:DB中實際使用的是擴展線性HASH算法(extended linear hashing),可以根據HASH表的增長進行適當的調整。關鍵字可以爲任意的數據結構。

Recno算法: 要求每一個記錄都有一個邏輯紀錄號,邏輯紀錄號由算法本身生成。實際上,這和關係型數據庫中邏輯主鍵通常定義爲int AUTO型是同一個概念。Recho建立在B+樹算法之上,提供了一個存儲有序數據的接口。記錄的長度可以爲定長或不定長。

Queue算法:和Recno方式接近, 只不過記錄的長度爲定長。數據以定長記錄方式存儲在隊列中,插入操作把記錄插入到隊列的尾部,相比之下插入速度是最快的。

對算法的選擇首先要看關鍵字的類型,如果爲複雜類型,則只能選擇B+樹或HASH算法,如果關鍵字爲邏輯記錄號,則應該選擇Recno或Queue算法。當工作集關鍵字有序時,B+樹算法比較合適;如果工作集比較大且基本上關鍵字爲隨機分佈時,選擇HASH算法。Queue算法只能存儲定長的記錄,在高的併發處理情況下,Queue算法效率較高;如果是其它情況,則選擇Recno算法,Recno算法把數據存儲爲平面文件格式。

DB常用函數使用範例

 

 

#include <db.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

void print_error(int ret) {
	if (ret != 0) {
		printf("Error: %s\n", db_strerror(ret));
	}
}

void init_DBT(DBT* key, DBT* data) {
	memset(key, 0, sizeof(DBT));
	memset(data, 0, sizeof(DBT));
}

int main() {
	DB* dbp;
	DBT key, data;
	u_int32_t flags;
	int ret;
	char* fruit = "apple";
	int number = 15;
	typedef struct customer {
		int c_id;
		char name[10];
		char address[20];
		int age;
	} Customer;
	Customer cust;
	int key_cust_c_id = 1;
	cust.c_id = 1;
	strncpy(cust.name, "javer", 9);
	strncpy(cust.address, "chengdu", 19);
	cust.age = 32;
	// create db hander
	ret = db_create(&dbp, NULL, 0);
	print_error(ret);

	// create db flags
	flags = DB_CREATE;
	// create a db named single.db with B+ tree.
	ret = dbp->open(dbp, NULL, "single.db", NULL, DB_BTREE, flags, 0);
	print_error(ret);
	init_DBT(&key, &data);
	
	key.data = fruit;
	key.size = strlen(fruit) + 1;
	data.data = &number;
	data.size = sizeof(int);
	
	ret = dbp->put(dbp, NULL, &key, &data, DB_NOOVERWRITE);
	print_error(ret);
	
	//dbp->sync(dbp);
	
	init_DBT(&key, &data);
	key.data = fruit;
	key.size = strlen(fruit) + 1;

	ret = dbp->get(dbp, NULL, &key, &data, 0);
	print_error(ret);
	// the name type of DBT's data is void*.
	printf("The number = %d\n", *(int*)(data.data));
	
	if (dbp != NULL) {
		dbp->close(dbp, 0);
	}

	ret = db_create(&dbp, NULL, 0);
	print_error(ret);
	flags = DB_CREATE;
	// create a db names "complex.db" with hash.
	ret = dbp->open(dbp, NULL, "complex.db", NULL, DB_HASH, flags, 0);
	print_error(ret);
	init_DBT(&key, &data);
	key.size = sizeof(int);
	key.data = &(cust.c_id);
	data.size = sizeof(Customer);
	data.data = &cust;
	ret = dbp->put(dbp, NULL, &key, &data, DB_NOOVERWRITE);
	print_error(ret);

	memset(&cust, 0, sizeof(Customer));
	
	key.size = sizeof(int);
	key.data = &key_cust_c_id;
	data.data = &cust;
	data.ulen = sizeof(Customer);
	data.flags = DB_DBT_USERMEM;
	dbp->get(dbp, NULL, &key, &data, 0);
	print_error(ret);

	printf("c_id = %d name = %s address = %s age = %d\n",
		cust.c_id, cust.name, cust.address, cust.age);
	if (dbp != NULL) {
		dbp->close(dbp, 0);
	}

	typedef struct Pair {
		long key;
		double value;
	} Pair;
	Pair pairs[3] = {{1, 0.1}, {2, 0.2}, {3, 0.3}};
	ret = db_create(&dbp, NULL, 0);
	print_error(ret);
	flags = DB_CREATE;
	// create a db names "complex2.db" with hash.
	ret = dbp->open(dbp, NULL, "/tmp/complex2.db", NULL, DB_HASH, flags, 0);
	print_error(ret);
	init_DBT(&key, &data);
	key.size = sizeof(int);
	key.data = &(key_cust_c_id);
	data.size = sizeof(Pair) * 3;
	data.data = pairs;
	ret = dbp->put(dbp, NULL, &key, &data, DB_NOOVERWRITE);
	print_error(ret);

	memset(&cust, 0, sizeof(Customer));
	
	key.size = sizeof(int);
	key.data = &key_cust_c_id;
	dbp->get(dbp, NULL, &key, &data, 0);
	print_error(ret);

	int size = data.size / sizeof(Pair);
	Pair* pp = (Pair*)data.data;
	for (int i = 0; i < size; ++i) {
		printf("key = %ld, value = %f\n", pp[i].key, pp[i].value);
	}
	if (dbp != NULL) {
		dbp->close(dbp, 0);
	}
	return 0;
}
       

 DB遊標使用範例

遊標是依賴於數據庫句柄的,應用程序代碼框架如下:

/* 定義一個遊標變量 */
DBC * cur;
/* 首先打開數據庫,再打開遊標 */
dbp->open(dbp, ……);
dbp->cursor(dbp, NULL, &cur, 0);
	
/* do something with cursor */
/* 首先關閉,在關閉數據庫 */
cur->c_close(cur);
dbp->close(dbp, 0);

 在遊標打開後,可以以多種方式遍歷特定記錄。

Memset(&key, 0, sizeof(DBT));
Memset(&data, 0, sizeof(DBT));
/* 因爲KEY和DATA爲空,則遊標遍歷整個數據庫記錄 */
While((ret = cur->c_get(cur, &key, &data, DB_NEXT)) == 0)
{
	/* do something with key and data */
}

 當想查詢特定關鍵字對應的記錄,則應對關鍵字賦值,並把cur->c_get()函數中標誌位設置爲DB_SET。例如:

key.data = "xxxxx";
key.size =  XXX;
While((ret = cur->c_get(cur, &key, &data, DB_SET)) == 0)
{
	/* do something with key and data */
}

 遊標的作用還有很多,如查詢多重記錄,插入/修改/刪除記錄等。

DB環境使用範例

本文前面已說明環境是DB數據庫的包裝器,提供多種高級功能。應用程序代碼框架如下:

/* 定義一個環境變量,並創建 */
DB_ENV *dbenv;
db_env_create(&dbenv, 0);
  
/* 在環境打開之前,可調用形式爲dbenv->set_XXX()的若干函數設置環境 */
/* 通知DB使用Rijndael加密算法(參考資料>)對數據進行處理 */
dbenv->set_encrypt(dbenv, "encrypt_string", DB_ENCRYPT_AES);
/* 設置DB的緩存爲5M */
dbenv->set_cachesize(dbenv, 0, 5 * 1024 * 1024, 0);
/* 設置DB查找數據庫文件的目錄 */
dbenv->set_data_dir(dbenv, "/usr/javer/work_db");
/* 打開數據庫環境,注意後四個標誌分別指示DB啓動日誌、加鎖、緩存、事務處理子系統 */
dbenv->open(dbenv,home,DB_CREATE|DB_INIT_LOG|DB_INIT_LOCK| DB_INIT_MPOOL|DB_INIT_TXN, 0);
  
/* 在環境打開後,則可以打開若干個數據庫,所有數據庫的處理都在環境的控制和保護中。
注意db_create函數的第二個參數是環境變量 */
db_create(&dbp1, dbenv, 0);
dbp1->open(dbp1, ……);
db_create(&dbp2, dbenv, 0);
dbp1->open(dbp2, ……);
/* do something with the database */
/* 最後首先關閉打開的數據庫,再關閉環境 */
dbp2->close(dbp2, 0);
dbp1->close(dbp1, 0);
dbenv->close(dbenv, 0);

 DB軟件的安裝和編譯

CentOS下運行

yum install db*

 參考資料

http://www.ibm.com/developerworks/cn/linux/l-embdb/index.html

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章