BerkeleyDB庫簡介
BerkeleyDB(簡稱為BDB)是一種以key-value為結構的嵌入式數據庫引擎:
- 嵌入式:bdb提供了一系列應用程序接口(API),調用這些接口很簡單,應用程序和bdb所提供的庫一起編譯/鏈接成為可執行程序;
- NOSQL:bdb不支持SQL語言,它對數據的管理很簡單,bdb數據庫包含若干條記錄,每條記錄由關鍵字和數據(key-value)兩部分構成。數據可以是簡單的數據類型,也可以是復雜的數據類型,例如C語言的結構體,bdb對數據類型不做任何解釋,完全由程序員自行處理,典型的C語言指針的自由風格;
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應用程序的通用操作,內存共享緩沖池子系統可以用於在內存中基於頁面的文件緩沖。
BDB可以分為幾個子系統:
- 存儲管理子系統 (Storage Subsystem)
- 內存池管理子系統 (Memory Pool Subsystem)
- 事務子系統 (Transaction Subsystem)
- 鎖子系統 (Locking Subsystem)
- 日志子系統 (Logging Subsystem)
BDB的每一個基礎功能模塊都被設計為獨立的,也即意味着其使用領域並不局限於BDB本身,例如加鎖子系統可以用於非BDB應用程序的通用操作,內存共享緩沖池子系統可以用於在內存中基於頁面的文件緩沖。
BDB庫的安裝方法:從官網下載、解壓后執行下面的命令
cd build_unix
../dist/configure make make install
DB缺省把庫和頭文件安裝在目錄 /usr/local/BerkeleyDB.6.1/ 下,使用下面的命令就可正確編譯程序:
gcc test.c -I/usr/local/BerkeleyDB.6.1/include/ -L/usr/local/BerkeleyDB.6.1/lib/ -ldb -lpthread
下面是一個BDB API使用的例子:
#include <db.h> #include <stdio.h> #include <string.h> #include <stdlib.h> #include <pthread.h> typedef struct customer { int c_id; char name[10]; char address[20]; int age; } CUSTOMER; /* 數據結構DBT在使用前,應首先初始化,否則編譯可通過但運行時報參數錯誤 */ void init_DBT(DBT * key, DBT * data) { memset(key, 0, sizeof(DBT)); memset(data, 0, sizeof(DBT)); } int main(void) { DB_ENV *dbenv; DB *dbp; DBT key, data; int ret = 0; int key_cust_c_id = 1; CUSTOMER cust = {1, "chenqi", "beijing", 30}; /* initialize env handler */ if (ret = db_env_create(&dbenv, 0)) { printf("db_env_create ERROR: %s\n", db_strerror(ret)); goto failed; } u_int32_t flags = DB_CREATE | DB_INIT_MPOOL | DB_INIT_CDB | DB_THREAD;; if (ret = dbenv->open(dbenv, "/data0/bdb_test", flags, 0)) { printf("dbenv->open ERROR: %s\n", db_strerror(ret)); goto failed; } /* initialize db handler */ if (ret = db_create(&dbp, dbenv, 0)) { printf("db_create ERROR: %s\n", db_strerror(ret)); goto failed; } flags = DB_CREATE | DB_THREAD; if (ret = dbp->open(dbp, NULL, "single.db", NULL, DB_BTREE, flags, 0664)) { printf("dbp->open ERROR: %s\n", db_strerror(ret)); goto failed; } /* write record */ /* initialize DBT */ init_DBT(&key, &data); key.data = &key_cust_c_id; key.size = sizeof(key_cust_c_id); data.data = &cust; data.size = sizeof(CUSTOMER); if (ret = dbp->put(dbp, NULL, &key, &data, DB_NOOVERWRITE)) { printf("dbp->put ERROR: %s\n", db_strerror(ret)); goto failed; } /* flush to disk */ dbp->sync(dbp, 0); /* get record */ init_DBT(&key, &data); key.data = &key_cust_c_id; key.size = sizeof(key_cust_c_id); data.flags = DB_DBT_MALLOC; if (ret = dbp->get(dbp, NULL, &key, &data, 0)) { printf("dbp->get ERROR: %s\n", db_strerror(ret)); goto failed; } CUSTOMER *info = data.data; printf("id = %d\nname=%s\naddress=%s\nage=%d\n", info->c_id, info->name, info->address, info->age); /* free */ free(data.data); if(dbp) { dbp->close(dbp, 0); } if (dbenv) { dbenv->close(dbenv, 0); } return 0; failed: if(dbp) { dbp->close(dbp, 0); } if (dbenv) { dbenv->close(dbenv, 0); } return -1; }
上面的例子中使用了很多BDB庫中的API,在下面會再具體介紹它們。
訪問方法
訪問方法對應了數據在硬盤上的存儲格式和操作方法。在編寫應用程序時,選擇合適的算法可能會在運算速度上提高1個甚至多個數量級。大多數數據庫都選用B+樹算法,DB也不例外,同時還支持HASH算法、Recno算法和Queue算法。接下來,我們將討論這些算法的特點以及如何根據需要存儲數據的特點進行選擇。
- BTree:有序平衡樹結構;
- Hash:擴展線性哈希表結構(extended linear hashing);
- Queue:由有固定長度的記錄組成的隊列結構,每個記錄使用一個邏輯序列號作為鍵值,邏輯紀錄號由算法本身生成,這和關系型數據庫中邏輯主鍵通常定義為int AUTO型是同一個概念;支持在隊尾快速插入,和從隊首取出(或刪除)記錄;並提供記錄級別的加鎖操作,從而支持對隊列的並發訪問。
- Recno:同時支持固定長度的記錄和變長記錄,並且提供支持flat text file的永久存儲和數據在讀時提供一個快速的臨時存儲空間;
說明:
BTree和Hash的key和value都支持任意復雜類型,並且也允許存在key重復的記錄;
Queue和Recno的key只能是邏輯序列號,兩者基本上都是建立在Btree算法之上,提供存儲有序數據的接口。前者的序列號是不可變的,后者的序列號可以是可變,也可以是不變;
可變,指的是當記錄被刪除或者插入時,編號改變;不變,指的是不管數據庫如何操作,編號都不改變。在Queue算法中編號總被不變的。在Recno算法中編號是可變的,即當記錄被刪除或者插入時,數據庫里的其他記錄的編號也可能會改變。
另外,Queue的value為定長結構,而Recno的value可以為定長,也可以為變長結構;
對算法的選擇首先要看關鍵字的類型,如果為復雜類型,則只能選擇BTree或HASH算法,如果關鍵字為邏輯記錄號,則應該選擇Recno或Queue算法。
當工作集key有序時,BTree算法比較合適;如果工作集比較大且基本上關鍵字為隨機分布時,選擇HASH算法。
Queue算法只能存儲定長的記錄,在高的並發處理情況下,Queue算法效率較高;如果是其它情況,則選擇Recno算法,Recno算法把數據存儲為flat text file。
Access Method |
Description |
Choosing Occasion |
BTree |
關鍵字有序存儲,並且其結構能隨數據的插入和刪除進行動態調整。為了代碼的簡單,Berkeley DB沒有實現對關鍵字的前綴碼壓縮。B+樹支持對數據查詢、插入、刪除的常數級速度。關鍵字可以為任意的數據結構。 |
1、 當Key為復雜類型時。 2、 當Key有序時。 |
Hash |
DB中實際使用的是擴展線性HASH算法(extended linear hashing),可以根據HASH表的增長進行適當的調整。關鍵字可以為任意的數據結構。 |
1、 當Key為復雜類型。 2、 當數據較大且key隨機分布時。
|
Recno |
要求每一個記錄都有一個邏輯紀錄號,邏輯紀錄號由算法本身生成。相當於關系數據庫中的自動增長字段。Recho建立在B+樹算法之上,提供了一個存儲有序數據的接口。記錄的長度可以為定長或不定長。 |
1、 當key為邏輯記錄號時。 2、 當非高並發的情況下。 |
Queue |
和Recno方式接近, 只不過記錄的長度為定長。數據以定長記錄方式存儲在隊列中,插入操作把記錄插入到隊列的尾部,相比之下插入速度是最快的。 |
1、當key為邏輯記錄號時。 2、定長記錄。 3、 高並發的情況下。 |
數據結構
數據庫環境句柄結構DB_ENV:環境在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中核心數據結構在使用前都要初始化,隨后可以調用結構中的函數(指針)完成各種操作,最后必須關閉數據結構。從設計思想的層面上看,這種設計方法是利用面向過程語言實現面對對象編程的一個典范。
DB_ENV *dbenv; // 環境句柄 DB *dbp; // 數據庫句柄 DBT key, value; // 紀錄結構 DBC *cur; // 游標結構
數據庫每條記錄包含兩個DBT結構,一個是key,一個是value。
typedef struct { void *data; // 數據buf u_int32_t size; // 數據大小 u_int32_t ulen; // u_int32_t dlen; // 數據長度 u_int32_t doff; // 數據開始處 u_int32_t flags; } DBT;
數據庫環境
- 在一個磁盤文件中包含多個數據庫;
- 多進程和多線程支持;
- 事務處理;
- 高可用支持(主從庫復制);
- 日志系統(可用於數據庫異常恢復);
DB_ENV *dbenv; db_env_create(&dbenv, 0); // 創建數據庫環境句柄 dbenv->open(dbenv, path, flags, 0); // 打開數據庫環境, path是環境的目錄路徑, flag參數參考下面介紹 dbenv->close(dbenv, 0); // 關閉數據庫環境 dbenv->err(dbenv, ret, formart, ...); // 錯誤調試
DB_CREATE // 打開的環境不存在的話就創建它 DB_THREAD // 支持線程 DB_INIT_MPOOL // 初始化內存中的cache DB_INIT_CDB
BDB 環境的使用例子:
/* 定義一個環境變量,並創建 */ 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"); /* 設置出錯時的回調函數 */ dbenv->set_errcall(dbenv, callback); /* 將錯誤信息寫到指定文件 */ dbenv->set_errfile(dbenv, file); /* 打開數據庫環境,注意后四個標志分別指示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數據庫是一組K-V記錄的集合,key和value都是DBT結構存儲的,與數據庫操作有關的API:DB* dbp; db_create(&dbp, dbenv, 0); // 獲取數據庫句柄 dbp->open(dbp, NULL, filename, NULL, DB_BTREE, flags, 0); dbp->close(&dbp, 0); // 在關閉數據庫前,先關閉所有打開的游標 dbp->sync(dbp, 0) // 刷新cache,同步到磁盤,close操作會隱含調用該過程 dbp->remove(dbp, filename, NULL, 0) // 移除數據庫,不要移除已打開的數據庫 dbp->rename(dbp, oldname, NULL, newname, 0) // 數據庫重命名,不要重命名已打開的數據庫 dbp->put(dbp, NULL, &key, &data, DB_NOOVERWRITE); // DB_NOOVERWRITE不允許重寫已存在的key dbp->get(dbp, NULL, &key, &data, flags); // 如果存在key重復的記錄,只返回第一個,或者使用游標 dbp->del(dbp, NULL, &key, 0); // 刪除指定key的記錄 dbp->truncate(dbp, NULL, u_int32_t* count, 0); // 刪除所有記錄,count中返回被刪除的記錄個數 dbp->get_open_flags(dbp, &open_flags); // 獲取打開的flags,僅對已打開的數據庫才有意義 dbp->set_flags(dbp, flags); // 設置打開的flags
DB_CREATE //如果打開的數據庫不存在,就創建它;不指定這個標志,如果數據庫不存在,打開失敗! DB_EXC //與DB_CREATE一起使用,如果打開的數據庫已經存在,則打開失敗;不存在,則創建它; DB_RDONLY //只讀的方式打開,隨后的任何寫操作都會失敗; DB_TRUNCATE //清空對應的數據庫磁盤文件; DB_DUPSORT //
get方法返回DB_NOTFOUND時表示沒有匹配記錄,其最后一個參數flags:
DB_GET_BOTH // get方法默認只匹配key,該flag將返回key和data都匹配的第一條記錄
DB_MULTIPLE // get方法默認只返回匹配的第一條記錄,該flag返回所有匹配記錄
使用get方法時,data參數是DBT結構,該DBT的flags參數可以定義為:
DB_DBT_USERMEM // 使用自己的內存存儲檢索的data DB_DBT_MALLOC // 使用DB分配的內存,用完后要手動free
DB提供的內存對齊方式可能不符合用戶數據結構的需求,所以盡量使用我們自己的內存。
用DB_DBT_USERMEM方式改寫前面的例子:
/* get record */ CUSTOMER info; init_DBT(&key, &data); key.data = &key_cust_c_id; key.size = sizeof(key_cust_c_id); data.data = &info; data.ulen = sizeof(CUSTOMER); data.flags = DB_DBT_USERMEM; if (ret = dbp->get(dbp, NULL, &key, &data, 0)) { printf("dbp->get ERROR: %s\n", db_strerror(ret)); goto failed; } printf("id = %d\nname=%s\naddress=%s\nage=%d\n", info.c_id, info.name, info.address, info.age);
db_strerror(errno) // 將錯誤編碼映射成一個字符串 dbp->set_errfile(dbp, FILE*) // 設置錯誤文件 dbp->set_errcall(dbp, void(*)(const DB_ENV *dbenv, const char* err_pfx, const char* msg)) // 定義錯誤處理的回調函數 dbp->set_errpfx(dbp, format...) // 加上錯誤消息前綴 dbp->err(dbp, ret, format...) // 生成錯誤消息,並按優先級發給set_errcall定義的錯誤處理回調函數、set_errfile定義的文件、stderr; dbp->errx(dbp, format...) // 與dbp->err類似,但沒有返回值ret這個額外參數
錯誤消息由一個前綴(由set_errpfx定義)、消息本身(由err或errx定義)和一個換行符組成。
游標
DBC *cur;
dbp->cursor(dbp, NULL, &cur, 0); // 初始化游標對象 cur->close(cur); // 關閉游標 cur->get(cur, &key, &data, flags); // 迭代記錄,當沒有可迭代的記錄時,返回DB_NOTFOUND cur->put(cur, &key, &data, flags); cur->del(cur, 0); // 刪除游標指向的記錄
DB_NEXT // 從第一條紀錄遍歷到最后一條紀錄; DB_PREV // 逆序遍歷,從最后一條紀錄開始;
DB_SET // 移動游標到鍵值等於給定值的第一條紀錄; DB_SET_RANGE // 如果數據庫使用BTREE的算法,移動游標到鍵值大於或等於給定值的紀錄集合; DB_GET_BOTH // 移動游標到鍵值和數據項均等於給定值的第一條記錄; DB_GET_BOTH_RAGNE // 移動游標到鍵值等於給定值,數據項大於或等於給定值的紀錄集合; DB_NEXT_DUP // 獲取下一個key重復的記錄; DB_PREV_DUP // 獲取上一個key重復的記錄; DB_NEXT_NODUP // 獲取下一個key不重復的記錄; DB_PREV_NODUP // 獲取上一個key不重復的記錄;
DB_NODUPDATA // 如果插入的key已存在,返回DB_KEYEXIST,如果不存在,則記錄的插入順序由其在數據庫的插入順序決定; DB_KEYFIRST // 在key重復的集合里面放在第一個位置; DB_KEYLAST // 在key重復的集合里面放在最后一個位置; DB_CURRENT // replace
secondary數據庫
建立secondary database之后,如果在primary database中新增或刪除記錄,會觸發對secondary database的更新。
注意:我們不能直接更新secondary database,任何寫secondary database的操作都會失敗,secondary database的變更需要通過修改primary database實現。但這里有一個例外,允許在secondary database中刪除記錄。
primary_dbp->associate(primary_dbp, NULL, second_dbp, key_creator, 0); int key_creator(DB* dbp, const DBT* pkey, const DBT* pdata, DBT* skey);
一個例子:
DB *dbp, *sdbp; /* Primary and secondary DB handles */ u_int32_t flags; /* Primary database open flags */ int ret; /* Function return value */ typedef struct vendor { char name[MAXFIELD]; /* Vendor name */ char street[MAXFIELD]; /* Street name and number */ char city[MAXFIELD]; /* City */ char state[3]; /* Two-digit US state code */ char zipcode[6]; /* US zipcode */ char phone_number[13]; /* Vendor phone number */ char sales_rep[MAXFIELD]; /* Name of sales representative */ char sales_rep_phone[MAXFIELD]; /* Sales rep's phone number */ } VENDOR; /* Primary */ ret = db_create(&dbp, NULL, 0); if (ret != 0) { /* Error handling goes here */ } /* Secondary */ ret = db_create(&sdbp, NULL, 0); if (ret != 0) { /* Error handling goes here */ } /* Usually we want to support duplicates for secondary databases */ ret = sdbp->set_flags(sdbp, DB_DUPSORT); if (ret != 0) { /* Error handling goes here */ } /* Database open flags */ flags = DB_CREATE; /* If the database does not exist, create it.*/ /* open the primary database */ ret = dbp->open(dbp, NULL, "my_db.db", NULL, DB_BTREE, flags, 0); if (ret != 0) { /* Error handling goes here */ } /* open the secondary database */ ret = sdbp->open(sdbp, NULL, "my_secdb.db", NULL, DB_BTREE, flags, 0); if (ret != 0) { /* Error handling goes here */ } /* Callback used for key creation. Not defined in this example. See the next section. */ int get_sales_rep(DB *sdbp, /* secondary db handle */ const DBT *pkey, /* primary db record's key */ const DBT *pdata, /* primary db record's data */ DBT *skey) /* secondary db record's key */ { VENDOR *vendor;
/* First, extract the structure contained in the primary's data */ vendor = pdata->data;
/* Now set the secondary key's data to be the representative's name */ memset(skey, 0, sizeof(DBT)); skey->data = vendor->sales_rep; skey->size = strlen(vendor->sales_rep) + 1;
/* Return 0 to indicate that the record can be created/updated. */ return (0); } /* Now associate the secondary to the primary */ dbp->associate(dbp, NULL, sdbp, get_sales_rep, 0);
頁面大小
DB* dbp; dbp->set_pagesize() // 設置page size dbp->stat() // 查看page size
緩存
DB可以將那些經常訪問到記錄cache 到內存里面,從而加快讀寫速度。
dbp->set_cachesize(dbp, gbytes, bytes, ncache); // 通過數據庫句柄設置cache大小 dbenv->set_cachesize(dbp, gbytes, bytes, ncache); // 通過環境句柄設置cache大小(全局)