Berkeley DB Concurrent Data Store Application

来源:互联网 发布:tensorflow1.0 windows 编辑:程序博客网 时间:2024/04/27 22:01

  在完全没有必要用上恢复机制和事件的相关语义时,通常我们会用上可并发存储的数据库.对于这类的程序,BDB提供了释放死锁(deadlock-free),数读/单写(multiple-reader/single-writer)的接口.这意味着在同一时刻,可以有多个读者访问数据,但只能有一个来修改数据.这种机制对于应用程序来说是不可见的,BDB提供了必要的锁及阻塞机制的保证这种特性.

    要创建BDB并发数据存储应用程序,你需要用DB_ENV->open来初始化一个环境,并在该函数中指定DB_INIT_CDB及DB_INIT_MPOOL的标志,若在此时同时指定了DB_INIT_LOCK,DB_INIT_TXN,DB_RECOVER之类的子系统或恢复配置的标志将产生一个错误.所有相关的数据库必须在环境中打开,并同时将该环境句柄作为一个参数来指定.

    BDB通过执行相应的锁使释放死锁,数读/单写的机制对应用程序透明.当对一个并发数据存储程序进行写操作时,BDB一个基本的底层锁机制会起到很好的作用.

    BDB并发数据存储并不需要通过对整个数据库加锁的死锁探测器来避免死锁,而是确保了在同一时间只允许某一线程在拥有读锁的同时可以获得写操作的权利.所有打开的BDB游标都拥有一个读锁,以保证数据库不被它们修改;同样,所有通过DB->get操作在遍历数据库的时候暂时获得及释放一个读锁.由于读锁不会相互影响,多线程里的多个游标可以同时打开,而多个DB->get拥有同时对数据库进行操作的权利.
    
    为实现单线程读的规则,在某一个时间内允许一个线程的读锁升级为写锁,但是,BDB必须防止多个游标同时进行写操作.BDB并发数据存储与普通的操作唯一不同的地方是:在用DB->cursor创建一个游标时指定DB_WRITECURSOR标志.该标志使新建的游标成为一个写游标,也就是一个既可读又可写的游标.只有这样创建的游标才允许对数据库进行写操作,而同一时间只允许存在一个这样的游标.

    当一个写游标打开时,所有创建第二个写游标或普通写的操作将引起阻塞,直到第一个写游标关闭.而读游标却在写游标存在时正常的创建与使用.但是,任何方式的写操作,包括写游标以及直接用DB->put/DB->del的方法,将被阻塞到所有的读游标被关闭之后.防止读到由于同时读写可能造成的数据库不一致状态,以及多读/单写,就是这样被实现的.

    有了这些机制,BDB可以保证无死锁的并发数据库的使用,所以多线程可以无须自己保持同步及无死锁探测器的情况下自由读写数据库.由于BDB没有能力知道哪个游标属于哪个线程,所以需要留神以保证程序不会由于自己不无意间阻塞而被挂起或无法执行,下面是常常出现的错误:
    1. 在调用DB->put/DB->del时,有游标正打开.
    2. 试图在同一线程中存在被打开的写游标时打开另一写游标.
    注意:当线程中存在写游标时,在另一线程中打开写游标或调用DB->put/DB->del的操作是正确的,唯一的问题是,当在单独的线程中做这些事情时,该线程将被阻塞并且无法释放阻塞它的锁.
    3. 长期打开一个写游标.
    4. 不测试BDB的返回值.(正确的作法:当某游标操作返回错误码时,该游标应该立即关闭)
    5. 默认情况下,BDB的并发数据存储的锁是以单个库为基础的.基于这个原因:当通过不同线程/进程的游标以不同的顺序对环境中不同的数据库进行操作时,或在访问其他数据库时使某数据库的游标打开,可能导致程序挂起.当必须进行这种操作时,BDB应该用基于整个环境的锁,(在DB_ENV->set_flags时设置DB_CDB_ALLDB标志或DB_CONFIG文件).



出错处理:

    当进行数据存储或并发数据存储编程时,有必要考虑到因为某种原因某线程(这里的线程包括线程和进程)打开数据库句柄出错的情况.
   
    最简单的情况是处理数据存储或并发数据存储的系统错误,在这种情况下,无论是在环境中还是单独的数据库程序,当程序已经进行了修改操作但还没有把该修改存入到数据库中(DB->close,DB->sync,DB_ENV->memp_sync),该数据库可能处于腐烂状态.对于这种情况,在重新对数据库进行访问时,该数据库应该:
    1. 移出然后重新创建.
    2. 移出然后以最新的备份替代.
    3. 用DB->verify或db_verify实用程序(utility)进行校对.若数据库校对得不干净,可以用db_dump实用程序的-R或-r选项进行救援(salvaged).

    如果潜在的数据丢失是不可接受的,应用程序应该考虑BDB的事件数据存储方式(Transactional Data Store product),它提供了标准的事件耐久保证,包括出错后的可恢复性.

    另外,系统错误需要所有残余的数据库环境(任何不以DB_PRIVATE标志创建的环境)被移出.数据库环境的移出可以使用DB_ENV->remove.如果持续(persistent)的数据库环境被备份到了文件系统(也就是说该环境不是以DB_SYSTEM_MEM标志创建),该数据库环境同样可以用标准的系统函数通过删除环境文件来移除.
   
    第二种情况是数据存储(无论有没有数据库环境)程序出错,或不带数据库环境的并发数据存储程序出错.跟系统出错一样,在这种情况下,无论是在环境中还是单独的数据库程序,当程序已经进行了修改操作但还没有把该修改存入到数据库中,该数据库可能处于腐烂状态.这种情况的处理方式同系统出错.
   
    第三种情况是带数据库环境的并发数据存储程序出错.如果一个控制线程在退出之前没有关闭所有的数据库句柄,数据库环境的资源有可能被锁住.带数据库环境的并发数据存储程序有另一个选择来处理控制线程的错误退出:DB_ENV->failchk.
    如果由于控制线程出错导致数据库环境不可用,DB_ENV->failchk的调用将返回DB_RUNRECOVERY.如果由于线程出错,某数据结构互斥或数据库写锁仍被保留,应用程序不应该继续使用该数据库环境,因为如果继续使用可能导致其他控制线程convoying在被保留的锁之后.该函数的调用将释放掉所有已退出的控制线程遗留下来的数据库读锁(read locks).这种情况下,程序可以继续使用该数据库环境.并发数据存储程序在发生了控制线程错误之后应该调用DB_ENV->failchk,如果返回0,程序可以继续,如果返回DB_RUNRECOVERY,程序应该象处理系统错误一样来处理该数据库环境.



搭建数据存储/并发数据存储程序:

    在进行数据存储及并发数据存储编程时,架构判定(architecture decisions)意味着启动(startup)(清除所有现存数据库,现存数据库环境的移出以及创建新的环境)、系统或程序出错处理。清除意味着移出及重新创建数据库,恢复备份,以及有可能用到的确认(verification)及选择性的救援。

    精确的说,在非环境中的数据存储或并发数据存储是单线程的。这些程序的步骤是:启动、重建、恢复,以及有可能用到的确认及选择性的救援,然后一直运行到程序退出或者程序/系统出错。当程序/系统出错之后,该进程可以简单的重复这些步骤,我们这里就不继续讨论。

    数据库存储或并发数据库存储构建的第一个问题是清除所有现存数据库,现存数据库环境的移出以及创建新的环境。基于明显的原因,程序应该连续的重建、恢复,或者确认及选择性的救援其数据库。进一步来说,环境的移除及创建必须是单线程的,也就是说,一个单独的控制线程(或进程)应该在其他控制线程(进程)使用该环境之前移除及创建环境。BDB连续创建数据库环境是件很简单的事情,所以多控制线程对环境的创建将会排在某单线程之后连续进行(??)。

    移除数据库环境受限将该环境标志为失败(failed),这导致所有仍在该环境内运行的控制线程失败,然后返回到其调用程序。这个特性允许程序在不用考虑环境内部仍在运行的控制线程的情况下移除该环境。

    移除被另以线程使用的数据库环境的一个顾虑是被BDB库用到的互斥的类型。如果使用test-and-set互斥的数据库环境出错时,环境将被标志为失败,等待互斥的控制线程将马上注意到这个失败,返回一个BDB API的错误。如果使用互斥锁的数据库环境失败,底层的系统互斥调用不会为在某一控制线程锁死(hold the mutex dies)之后的等待线程解锁,所有等待一个互斥的线程可能在环境恢复以后永远挂起。程序的事件阻塞也有可能在一段合理的时间内没有注意到环境的恢复。有着这样的互斥调用的系统虽然不多,但是仍然存在;这种系统中的程序应该或用特定的程序来在线程恢复数据库环境时明确的终止使用错误环境的任何进程,或配置BDB使用test-and-set互斥,或使用看门狗进程、计时器来监视阻塞的进程以避免阻塞时间过长。

    在多控制线程同时移除及重建一个环境上不用操太多心,因为最后一个线程将移除它之前所有创建的线程。但是,在某些少量的程序中,会用一个单独的控制线程来查看已存在的数据库及移除环境,在这之后,程序启动一些进程,它们中的任何一个都可以创建环境。

    数据库环境必须在其中的数据库全被移除之后再移除。对环境的移除将导致所有在标志为失败的控制线程对BDB库的调用返回一个错误码到程序中。移除数据库受限确认旧环境中的控制线程没有与清除数据库的控制线程赛跑(race),可能的话,在清除操作完成以后覆盖它们。当程序构架及系统允许,基于一般的原则以及降低系统资源使用的目的,许多程序在移除失败的数据库环境之前杀死所有失败的数据库环境中的控制线程。新的环境在数据库被移除之前还是之后创建并没有关系。

    处理失败而调用数据库及数据库环境恢复之后,下一个有待处理的问题就是程序失败。当一个数据存储或并发数据存储程序的控制线程失败后,它可能在退出之后还保留着数据结构互斥或逻辑数据库锁。这些互斥或锁应该释放以避免其他余下的控制线程挂起。

    有三种思路来构建BDB数据存储及并发数据存储程序,由该程序是单进程、由一个进程孵化的一组多进程、或一些没有关系的进程成来决定:

    1. 单进程。

    该进程启动时,移除所有现存的数据库环境后再创建一个新的环境。然后清除数据库,打开环境中的数据库。该程序随后可以创建新的控制线程。这些新的控制线程可以或共享已经打开的数据库环境句柄,或自己创建。在这种构架里,数据库很少被多于一个控制线程打开或关闭;也就是说,它们是在只有一个线程运行的时候被打开,在只有一个线程(其它的都先退出了)时关闭(该线程关闭数据库及数据库环境)。

    这种构架最容易实现,因为进程的连载(serialization)很容易,而且出错探测不涉及到多进程。
    如果程序的线程模式允许线程失败后进程继续运行,可以用DB_ENV->failchk方法来判断数据库环境是不是还可用。如果程序没有调用DB_ENV->failchk或者调用后返回DB_RUNRECOVERY,程序需以系统失败的情况来处理:移除环境并创建新的环境,清除需要继续使用的数据库。一旦这些操作完成,其它控制线程可以继续(只要所有现存BDB句柄是第一次被删除<discarded>)或重启。
   
    2. 由一个进程孵化的一组多进程(线程)。
    这种架构需要控制线程创建的顺序被控制为数据库环境移除、数据库环境创建、数据库清除。

    另外,该架构需要监控控制线程。如果某控制线程带着BDB句柄退出,程序需调用DB_ENV->failchk来判断环境是否可用。如果程序没有调用DB_ENV->failchk或者调用后返回DB_RUNRECOVERY,程序需以系统失败的情况来处理:移除环境并创建新的环境,清除需要继续使用的数据库。一旦这些操作完成,其它控制线程可以继续(只要所有现存BDB句柄是第一次被删除<discarded>)或重启。

    一个简单的构建相关进程的方式是在系统第一次启动时先创建一个监测进程(通常是script),移除所有现存的数据库环境后再创建一个新的环境,清除数据库,然后创建进程/线程。最初的线程的责任只是等待控制线程启动,然后确认它们不错误的退出。当某控制线程退出时,监测进程调用DB_ENV->failchk,如果环境不可用,该进程杀死所有使用该环境的控制线程,清理,然后启动新的控制进程继续工作。

    3. 一些没有关系的进程,这时最难运用的构架,因为在有些系统中查找与管理无关系的进程的难度极大。

    一种解决方案是打开新的BDB句柄时将某控制线程ID记录下来。比如,一个初始化监测进程可以打开/创建数据库环境,清理数据库以及创建一个守护文件。所有要使用环境的工作进程都要访问这个守护文件。若守护文件不存在,工作进程将等待该文件被创建。一旦守护文件孙子,工作进程向监测进程注册其进程ID(通过共享内存,IPC或其它注册机制),然后工作进程可以打开环境句柄进行访问,在使用结束之后,取消其进程ID的注册。监测进程周期性的监测以确保没有工作进程在使用环境的时候失败。如果工作进程在使用环境时失败,监测进程移除守护文件,杀死所有注册过的工作进程,清理环境及数据库,最后创建一个新的守护文件。

    这种途径的缺点在于,在有些系统中,很难判断一个无关进程是否还在运行。比如,POSIX系统禁止向无关进程直接信号通讯。管理无关进程的方法是找到该进程掌握的系统资源,当该进程死时,该资源必须发生变化。在POSIX系统中,flock-或者fcntl-style就是这种资源,正如Windows中的LockFile。在其它系统中,可能不得不用到其它与进程相关的信息,比如文件引用数目或修改次数。最差的情况是,控制线程不得不向监测进程周期性的重复注册:如果监测线程在指定的一段时间内没有接收到任何控制线程的信息,它就清除环境。

    如果监测共享数据库环境的进程的方法不切实际,可能需要通过管理环境来探测控制线程是否带着一个BDB句柄失败。这种方式需要一个监测进程周期性的调用DB_ENV->failchk,若返回DB_RUNRECOVERY,监测进程清除该环境。

    这种途径的缺点在于,所有使用环境的控制线程必须通过DB_ENV->set_thread_id指定一个ID函数及一个is-alive函数。换一种说法,BDB库必须为每一个控制线程指定一个唯一的ID,然后还要判断该控制线程是不是还在运行。这使得在使用多种编程语言的程序中很难提供信息,也使程序的移植变得很难。
   
    很明显,当用一个进程来监测其它控制线程的时候,该进程的代码必须简单而且容易测试,因为程序可能因为该进程的失败而挂起。

 

 

 

 1   自己的理解

     CDB 线程并发方式 ,多个线程读,单线程写,所有的游标只读,只允许一个去写,而且同时排斥其他写的方式。

 2  DB_INIT_CDB 和 DB_INIT_LOCK 只能选择一种。

 3 内部提供 释放死锁 方式。