嵌入式資料庫系統Berkeley DB
層級: 初級
施聰 (javer@163.com), 進階程式員、網路設計師
2005 年 4 月 01 日
Berkeley DB是曆史悠久的嵌入式資料庫系統,主要應用在UNIX/LINUX作業系統上,其設計思想是簡單、小巧、可靠、高效能。本文是對DB開發的一個入門級指南,重點討論了DB的核心資料結構和資料訪問演算法,並通過實際的代碼示範如何使用DB。最後有一個對DB的簡單總結,並提出作者對工具選擇的一些感想。
UNIX/LINUX平台下的資料庫種類非常多,參考資料1中列舉了其中的大部分。通常,我們在設計UNIX/LINUX平台下的應用軟體時,如果資料種類繁多,資料與資料之間關係比較複雜,就會選用一些大型的企業級資料庫系統,如DB2,ORACLE、SYBASE等,如果軟體規模不大,就傾向選用如MYSQL、POSTGRESQL等中小型資料庫。例如使用PHP/PERL + MYSQL/POSTGRESQL設計網站基本上是一個很常規的做法。但是,當應用軟體管理的資料類型較少(特別注意:這並不是說需要管理的資料量小),資料管理本身不複雜,且對資料操作要求高效率,則由大名鼎鼎的Berkeley(美國加州大學伯克利分校)開發的 Berkeley 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提供了一系列應用程式介面(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結構中包含了大量的資料庫處理函數指標,大多數形式為 (*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中核心資料結構在使用前都要初始化,隨後可以調用結構中的函數(指標)完成各種操作,最後必須關閉資料結構。從設計思想的層面上看,這種設計方法是利用面向過程語言實現面對對象編程的一個典範。
在資料庫領域中,資料訪問演算法對應了資料在硬碟上的儲存格式和操作方法。在編寫應用程式時,選擇合適的演算法可能會在運算速度上提高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演算法把資料存放區為一般影像檔案格式。
#include <db.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
/* DB的函數執行完成後,返回0代表成功,否則失敗 */
void print_error(int ret)
{
if(ret != 0)
printf("ERROR: %s/n",db_strerror(ret));
}
/* 資料結構DBT在使用前,應首先初始化,否則編譯可通過但運行時報參數錯誤 */
void init_DBT(DBT * key, DBT * data)
{
memset(key, 0, sizeof(DBT));
memset(data, 0, sizeof(DBT));
}
void main(void)
{
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;
/* 首先建立資料庫控制代碼 */
ret = db_create(&dbp, NULL, 0);
print_error(ret);
/* 建立資料庫標誌 */
flags = DB_CREATE;
/* 建立一個名為single.db的資料庫,使用B+樹訪問演算法,本段代碼示範對單一資料型別的處理 */
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();
init_DBT(&key, &data);
key.data = fruit;
key.size = strlen(fruit) + 1;
/* 從資料庫中查詢關鍵字為apple的記錄 */
ret = dbp->get(dbp, NULL, &key, &data, 0);
print_error(ret);
/* 特別要注意資料結構DBT的欄位data為void *型,所以在對data賦值和取值時,要做必要的類型轉換。 */
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;
/* 建立一個名為complex.db的資料庫,使用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);
}
遊標是依賴於資料庫控制代碼的,應用程式代碼架構如下:
/* 定義一個遊標變數 */
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_ENV *dbenv;
db_env_create(&dbenv, 0);
/* 在環境開啟之前,可調用形式為dbenv->set_XXX()的若干函數設定環境 */
/* 通知DB使用Rijndael密碼編譯演算法(參考資料4)對資料進行處理 */
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的官方網站http://www.sleepycat.com/下載最新的軟體包db-4.3.27.tar.gz,解壓到工作目錄,進入該目錄,依次執行下列三條命令即可。
../dist/configure
make
make install
執行make uninstall,則可卸載已安裝的DB軟體。
DB預設把庫和標頭檔安裝在目錄/usr/local/BerkeleyDB.4.3/下,使用gcc test.c -ggdb -I/usr/local/BerkeleyDB.4.3/include/ -L/usr/local/BerkeleyDB.4.3/lib/ -ldb -lpthread就可正確編譯器。如果讀者的測試主機作業系統為RED HAT9,則安裝的DB版本可能是4.0。特別要注意到這兩個版本的庫是不相容的。例如開啟資料庫函數DB->open(),在4.0版本中入參為6個,而在4.3版中則為7個(可自行比較兩個庫的標頭檔db.h中DB->open函數的定義)。因為在DB相關的應用程式中,open函數基本上都是要執行的,所以如果函數和版本不匹配,編譯肯定會出錯。當然,編譯完成後,可以使用命令ldd查看庫的依賴關係。
DB是一個具有工業強度的嵌入式資料庫系統,資料處理的效率很高。DB功能的穩定性曆經時間的考驗,在大量應用程式中使用便是明證。可以想見,在同等代碼品質的條件下,軟體的BUG數和代碼的長度是成正比的,相對幾十兆、幾百兆大型資料庫軟體,DB的只有不到500K的大小!
從實現功能上看,DB是輕量級資料庫系統,或可稱為"極" 輕量級資料庫系統。但是,我認為不能因此而心存輕視之意,所謂"尺有所短,寸有所長",以絕對角度比較工具之間的好壞是沒有什麼意義的,關鍵在於對工具的選擇和運用(似乎可以參考一下極限編程的思想)。也許,正確的"表達範式"應該是:在當前應用背景下,選擇這種工具是最合適的。
《Linux SQL Databases and Tools》
《Implementing a File System with the Berkeley DB》
《Performance Improvement of OpenLDAP Transactional Backend》
Rijndael密碼編譯演算法官方網站
施聰,成都人,進階程式員、網路設計師。從事基於UNIX/LINUX下的c/c++程式設計和資料庫建模工作已10年。可通過javer@163.com或memncmp@yahoo.com.cn和他聯絡。