BerkeleyDB(简称为BDB)是一种以key-value为结构的嵌入式数据库引擎:
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可以分为几个子系统:
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的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。
数据结构数据库环境句柄结构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; // 游标结构 数据库环境BDB环境是对一个或多个数据库的封装,环境对应一个目录,数据库对应该目录下面的一个文件。 环境支持:
与环境相关的API: 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, ...); // 错误调试 打开环境时的flags标志位: DB_CREATE // 打开的环境不存在的话就创建它DB_THREAD // 支持线程DB_INIT_MPOOL // 初始化内存中的cacheDB_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不允许重写已存在的keydbp->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 打开数据库时的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 // 使用自己的内存存储检索的dataDB_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接口调用成功通常返回0,失败返回非0值,此时可以检查错误码errno; 由系统调用失败引起的错误errno>0,否则errno<0。 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定义)和一个换行符组成。 游标如果DB数据库中存在键值相同的多条记录,使用dbp->get()方法默认只能获取一条记录(除非打开DB_MULTIPLE标志),这个时候有必要使用游标(cursor),游标可以迭代DB数据库中的记录。 DBC *cur; cur->get()方法中flags参数的取值: 1、迭代整个数据库中的纪录集合: DB_NEXT // 从第一条纪录遍历到最后一条纪录;DB_PREV // 逆序遍历,从最后一条纪录开始; 2、查找符合条件的记录集: cur->put()方法中flags参数的取值: DB_NODUPDATA // 如果插入的key已存在,返回DB_KEYEXIST,如果不存在,则记录的插入顺序由其在数据库的插入顺序决定;DB_KEYFIRST // 在key重复的集合里面放在第一个位置;DB_KEYLAST // 在key重复的集合里面放在最后一个位置;DB_CURRENT // replace secondary数据库通常,对DB数据库的检索都是基于key的,如果想通过value(或value的部分信息)来检索数据可以通过secondary database来实现。 如果我们把存放K-V记录的数据库称为 primary database,那么secondary database索引的是其它字段,而对应的value就是primary database的key。从secondary database中读取记录时,DB自动返回primary database中对应的value; 在创建secondary database时需要提供一个callback函数,用来创建key,这个key是根据primary database的key或value生成的。这个callback函数返回0时才允许索引到secondary中,我们可以在callback中返回DB_DONOTINDEX或其它错误码告诉secondary不要索引该记录。 建立secondary database之后,如果在primary database中新增或删除记录,会触发对secondary database的更新。 将secondary绑定到primary 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; 页面大小BDB记录的key和value都是存放在内存页(page)里面,所以页面大小(page size)对数据库性能有很大影响。 对BTree库,page size的理想大小至少是记录大小的4倍。 DB* dbp; dbp->set_pagesize() // 设置page sizedbp->stat() // 查看page size page size的影响: 1、overflow pages 溢出页(overflow pages)是用来存放那些单个page无法存放的kye或value的page。 如果page size设置过低,会产生溢出页,从而影响数据库性能。 2、locking page size对多线程(或多进程)的BDB应用也会产生影响,这是因为BDB使用了page级的加锁(Queue除外)。 通常一个page包含多条记录,如果某个线程正在访问一条记录,则该记录所在的page会被锁住,导致同一个page下面的其他记录也无法被其他线程访问。 如果page size设置过高,会加大锁发生的概率,但page size过小,会导致BTree的深大变大,同样损失性能。 3、I/O DB数据库的页面大小要和文件系统的block size一致; 缓存DB可以将那些经常访问到记录cache 到内存里面,从而加快读写速度。 dbp->set_cachesize(dbp, gbytes, bytes, ncache); // 通过数据库句柄设置cache大小dbenv->set_cachesize(dbp, gbytes, bytes, ncache); // 通过环境句柄设置cache大小(全局) |
|