by zhuwei
Berkeley数据库是一种基于开源的嵌入式数据库,使用库的形式加载到进程里,应用直接使用API方式操作数据库。应用软件和数据库之间没有网络交互过程,默认编译情况下,不支持SQL语句(可以通过编译选项支持SQL),节省了SQL语句解析过程。它属于非关系数据库,采用key-value的形式存储表项,一般情况下不存在多表复合查询。对资源稀缺的嵌入式设备来讲,相比较而言,从理论上具备性能的优越性。
图 1 框架组件
Berkeley提供五中数据访问格式,分别为:Btree、Hash、 Heap、Queue 、Recno。
Btree:对大多数通用数据工作时负载性良好,绝大多数应用选择该方法。按指定键值访问,数据记录具有排序性,支持批量插入/查找/删除,操作数据效率高。在删除时,由于数据是基于排序的,后面新插入的数据不一定就能复用原有删除数据后释放出来的页面节点,而去新分配页面节点,所以缺点是磁盘空间增长快。解决方法是删除一定数量的数据后,由应用程序主动整理并释放空闲的磁盘空间;或者等待整个页面全部删除后再次重复利用。
Heap:适用于受限的磁盘空间,节约磁盘空间。应用程序创建和删除数据条目大致相同时比较合适,适合无序性数据。还可以设定Heap的上限空间大小,当空间达到上限后磁盘空间不再增加,能够自动复用该空间内已释放空间。缺点是只能采用berkeley内部生成的record id来检索数据库,如果应用想指定key值来查询数据,必须建立二级索引库来检索数据,增加设计复杂性。
Hash:适用于无排序性数据,在整个数据库数据集比较小时,性能和Btree相近。若整个数据集比较大,超过了缓存大小,在相同应用数据量前提下,Btree需要的内部基础数据更多,执行的IO操作更多一些;但从应用查询数据的相关性角度来说,Btree的性能更高。譬如数据“AAA”、“AAB”和“ZZZ”,对于BTree来讲,查询到AAA的前提下再去查询AAB就很容易,效率很高;但HASH在查询AAA的前提下再去查询AAB就不一定那么容易;因为在大数据集的前提下,HASH的AAA和ZZZ可能处于同一个页面,而AAA和AAB可能不在同一个页面;但Btree中AAA和AAB绝大多数情况下是处于同一个页面,除非Btree的应用定制了自己特殊的排序算法(默认是字典序)。
Queue:适用于固定长度的数据元素,提供数据元素记录级的Lock,更好的支持并发性。
Recno:相比较Queue,不仅支持固定长度的数据元素,还支持变长数据元素以及纯文本存储。
事务用于保证系统崩溃时数据可恢复性、原子性(对并发数据要么改过完成要么不改过)、隔离性(一系列操作仅局限于当前事务,不同事务间不可操作)。
-
-
- 基于事务存储数据的应用模式
- 单进程模式
- 基于事务存储数据的应用模式
-
该模式下有一个控制线程维护DB环境和DB句柄,其他线程访问数据库容易序列化。当控制线程发生异常或退出时,应用可以调用failchlk()决策是否需要重新创建DB环境和DB句柄。
-
-
-
- 多进程模式
-
-
该模式需要创建一个监视进程,该进程负责数据库运行环境的创建、恢复,创建工作进程并保证工作进程不会意外退出。若工作进程在监视下运行异常或退出,监视进程调用failchlk()决策是否需要重新创建DB环境和DB句柄以及新的工作进程。
-
-
-
- 多进程广播模式
-
-
将相关的进程作为一个进程组(即同一个数据库环境中的多个进程),若其中一个进程异常或退出,进程组中的其他进程都会收到该进程故障通知,其他进程再次访问数据库环境时需要进行数据库环境恢复。该模式需要在编译时打开编译选项--enable-failchk_broadcast。
-
-
-
- 进程组模式
- 事务相关基础功能
-
-
- 死锁检测:set_lk_detect,db库运行死锁检测迭代器,当发生死锁时,拒绝优先级低的锁请求(死锁时释放哪种类型的锁请求,应用可以自己定制)。同时与该锁关联的事务需要中止。
代码片断如下:
- 检测点:txn_checkpoint。当事务提交时,首先将改动数据写入到日志中,然后写入到正式数据库,该条事务日志也属于无用状态但也有可能使能了log_archive而被删除。当使能检测点时,数据更改暂时不会写入到正式数据库中,而是被写入到备份数据库,而后统一写入到正式数据库中并认为是一个完成的检测点。当发生系统崩溃重新上电时,从上一次检测点以后开始重新提交数据或撤销上一次检测点之后的所有更改,进而保证数据库的正常使用(该功能及其浪费资源和效率)。通常的做法是给数据库环境单独创建一个线程,周期性调用txn_checkpoint。
典型代码片段如下:
- 日志文件移除:事务产生的日志文件删除或归档,调用log_archive函数,返回1个日志周期内有数据库访问记录的相关日志句柄,由应用处理.
典型代码片断如下:
- 数据恢复:在数据库上电时,正常运行恢复数据。在应用程序/硬件故障导致崩溃后再次上电极其重要。打开数据环境时设置DB_RECOVER标志,设置该标记后,数据库在初始化阶段会加载log日志文件里面的事务过程。
代码片段如下:
将运行时间过长或操作过大的事务,分解成独立的子事务。子事务继承所有父事务的锁,所以父事务在所有子事务中止或提交之后才能中止或提交。父事务提交或中止时,未操作的任何子事务将依照父事务的操作执行。
- 选择合适的访问方法,从并发性角度来讲按照锁的细化粒度来分优先级:队列 > Btree > Hash/Recno。
- 在常规访问方法(Btree/Queue/Hash)之外再使用记录号(Recno)会降低性能。
- 使用Btree进行并发设计时,为了尽量避免死锁或锁冲突。需要降低每个数据库页面的大小,并增加页面级锁。
- 为了提高并发性,提倡非事务读操作、非事务游标。
- 保持KEY/DATA表项不要太大,因为事务提交前后都会写事务操作日志及日志内容,如果KEY/DATA表项过大,事务日志同步的磁盘时间也会相应增长。
- 互斥锁的选择,尽量选择阻塞性锁,尽量放弃非阻塞性锁,这样避免CPU空转时间。譬如选择优先级:互斥锁 > 自旋锁。
- 日志缓冲区大小以及日志文件的位置设定,设置大一点的日志缓冲区能够提高应用程序吞吐量。日志文件单独存放一个路径文件夹,降低DB对目录文件的无效遍历。
- 并发过程中,同一个页读写频繁时,读or写操作经常会发生等待。需要使用memp_trickle函数打开内存池管理功能,将页面数据达到一定比例后,提前写入磁盘并清理或提前读入磁盘数据。可以通过memp_stat来获取页面缓存数据进行分析,是否读写操作经常发生等待。
事务吞吐量是衡量数据库系统运行的速度指标,即每秒钟完成的事务个数。BerkeleyDB影响这个指标的因素为DB文件和日志文件。提高事务吞吐量的方法:
- 将一次文件操作封装在一个事务中;
- 放大/优化database cache,这样事务read和seek在多数情况下不涉及磁盘IO;
- DB文件和日志文件放在不同的磁盘上,便于读写DB文件和读写日志文件并发进行;本质上是存放在不同的磁盘扇区;
- 在应用程序允许的情况下,关闭访问\修改DB文件的同时更新DB文件时间(BerkeleyDB数据库不关心DB文件更新时间);
- 升级硬件,控制器和总线性能较大影响BerkeleyDB性能。磁盘的寻道时间长短是BerkeleyDB真正的性能瓶颈;
- 关闭事务提交时写log日志,使用DB_TXN_NOSYNC或DB_TXN_WRITE_NOSYNC关闭。存在的风险是数据库在recover期间,最近提交的一些事务可能会丢失,影响数据库的完整性。
- 设置运行事务处于激活状态个数
pMyEnv->set_tx_max函数。
- 打开事务
pMyEnv->txn_begin获取事务句柄。
- 设置事务最大超时事件
pTxn->set_timeout函数,若有并发设计时也是有效避免死锁的一种方法。
- 提交事务
pTxn->commit函数。
- 撤销事务
pTxn->abort函数。
一个打开的事务句柄一旦发生提交/撤销后,不能再次复用该句柄。
- Lock子系统
Lock子系统提供进程间\进程内并发性控制机制。在并发设计中,锁的粒度越细,表明并发的性能越好。在BerkeleyDB中,除了队列访问方法以外,锁粒度都是以数据库页面为单位的。
- DB初始化时指定(DB_INIT_LOCK| DB_INIT_TXN)事务中使用Lock,指定(DB_INIT_LOCK)在非事务中使用Lock;
- set_lk_detect函数打开死锁检测;
- set_timeout函数指定锁最大锁定时间;
- set_mem_init函数初始化该环境中允许同时申请的最大锁个数;
- Lock调试:使用lock_stat函数统计当前DB文件中的所有Lock信息。
BerkeleyDB支持独占锁和非独占锁,独占锁为write操作,非独占锁为read操作,但非独占锁锁定时不允许锁对象被修改。如下图所示:
事务中的锁资源直到该事务被commit/abort;非事务锁资源直到操作结束;游标锁直到游标移动到下一个位置。
如下图,TxnA在锁定002期间去锁定003,TxnB在锁定003期间去锁定002,容易造成死锁。
-
-
-
- 死锁的解决办法
-
-
- 配置死锁检测;
- 配置事务/锁超时时间;
- 应用发现死锁时,对事务做abort操作,代码片断如下:
- Buffer Pool子系统
共享内存缓冲池,提供面向页面、共享和缓存文件功能。该子系统能够有效降低磁盘IO频率。
- Log子系统
BerkeleyDB使用Log记录每个事务操作,用于保证DB库的可恢复性。每个本地日志文件对应一个DB_LSN结构,每个日志文件中的每条记录对应一个序列号,存储在DB_LSN结构中。
-
-
- Log配置
-
- set_lg_regionmax()函数设置日志在磁盘上区域最大值,默认是60K;
- set_lg_max()函数设置单个文件最大值,默认值是10M;
- set_lg_bsize函数设置单个log在内存中的大小;
- set_lg_dir函数设置log日志路径目录;
- set_lg_filemode函数设置创建log时文件模式;
- log_set_config函数配置log的控制参数。
- 编译:
- BerkeleyDB库跨操作系统的移植:新操作系统部分接口函数的更改;新操作系统是否支持POSIX功能;新操作系统上是否支持虚拟地址空间,考虑多进程访问共享内存的情况;新操作系统上同步原语的选择,是否支持阻塞性互斥,将影响到CPU的开销;新操作系统上可能编译器是非标准的,产生编译错误时需要修改源代码。
- BerkeleyDB库的基本使用
代码片段如下:
-
- 在环境句柄下建立DB文件库,代码片段如下:
-
- DB文件表项插入操作
代码片段如下:
DB文件表项遍历游标
这里为了提升效率,使用非事务游标。代码片段如下:BD文件表项删除
代码片断如下:
-
- BD文件表项删除
代码片断如下
g_pMyEnv->memp_sync(g_pMyEnv,NULL),该函数对环境下内存中所有页数据扫描,若有修改则同步到磁盘文件。
pDbp->sync(pHandle->pDbp,0),该函数只针对本DB文件库中已修改的页面数据同步到磁盘文件。
- 提高可靠性,网络上存在多个相同数据库,任何一个数据库故障后,应用程序仍然能够访问到完整数据;
- 提高数据库读取性能,应用层可以访问离自己最近的数据库副本,即边缘侧数据库,节省网络过程;
- 提高事务commit性能,这种情况是异步提交到网络数据库中,应用不用等待DISK IO完成;
- 提高数据持久性保证,每提交一个写数据到数据库,网络上会有多个副本写入本地数据库,防止单点数据库或硬件故障导致无法访问数据。
复制管理器组件提供多线程网络通讯,基于TCP/IP SOCKET通道来实现,所以每个复制管理器最多支持1000个数据库副本通道。
-
-
-
- 基本组网
-
-
客户端站点不承担应用程序写数据库操作,若遇到写数据库操作便将写数据库操作转发给主站点,由主站点写操作后同步到客户端站点。
-
-
-
- 主站点的选取方法
- 手工指定主站点
- 选举主站点
- 主站点的选取方法
-
-
选举成为主站点的前提:复制组中没有主站点、被选举者必须有最新的日志环境、被选举者必须得到复制组中最多的投票。
复制组选举主站点的时机:当复制组中没有主站点或复制组上电启动时。
客户端站点切换到主站点:
- 复制组中新增一个客户端站点,配置成最高优先级;
- 等待该客户端站点获得主站点的日志Log最新副本;
- 关闭当前主站点,触发剩余客户端站点进行选举主站点;
- 启动原有主站点,由于复制组在c)中已选举了主站点,所以原有主站点启动后是客户端站点角色进入复制组。
若复制管理器组件提供的复制功能能够无法满足应用,应用可以自编写代码和流程进行数据库复制。
\zhuwei\ecn_svn\code_commit\opensource\berkeleyDb\db-18.1.40\docs