Berkeley DB 示例程序详解(2)

// File TxnGuide.cpp
/*
* 这个例子程序是Berkeley DB的示例程序之一(DB/example_cxx/txn_guide/TxnGuide.cpp),
* 它演示了如何使用Berkeley DB的事务功能,以及如何做死锁检测和处理。
*
* 原始代码中有一些英文注释,但是对于初学者还是不够详细,我没有删除原来
* 的注释,而且添加了针对每一个Berkeley DB操作的更加详细的说明,请参考。
*
* 代码的非关键部分都已删除,所以这里的内容
* 无法直接编译运行。可以直接编译运行的版本我会放到空间的附件中。
*
*
* 用词约定:
* 本文提到的“数据库”是指Berkeley DB的database,相当于关系数据库的一个表。
* 一个数据库当中保存着很多个key/data pair,相当于关系数据库的一个表当中
* 保存着很多条记录。也就是说一个key/data pair相当于关系数据库的一条记录。
* 而数据库环境(DbEnv)是指Berkeley Db的执行环境,相当于一个关系数据库管理系统。
* 事务或者数据库事务与关系数据库中的事务语义完全相同,拥有ACID属性。
*/

/* 可移植的线程管理宏定义,它支持Win32线程API 以及pthread 线程库。*/
typedef HANDLE thread_t;
#define thread_create(thrp, attr, func, arg)                               /
   (((*(thrp) = CreateThread(NULL, 0,                                     /
       (LPTHREAD_START_ROUTINE)(func), (arg), 0, NULL)) == NULL) ? -1 : 0)
#define    thread_join(thr, statusp)                                          /
   ((WaitForSingleObject((thr), INFINITE) == WAIT_OBJECT_0) &&            /
   ((statusp == NULL) ? 0 :                            /
   (GetExitCodeThread((thr), (LPDWORD)(statusp)) ? 0 : -1)))

typedef HANDLE mutex_t;
#define mutex_init(m, attr)                                                /
   (((*(m) = CreateMutex(NULL, FALSE, NULL)) != NULL) ? 0 : -1)
#define mutex_lock(m)                                                      /
   ((WaitForSingleObject(*(m), INFINITE) == WAIT_OBJECT_0) ? 0 : -1)
#define mutex_unlock(m)         (ReleaseMutex(*(m)) ? 0 : -1)
#else
#include <pthread.h>
#include <unistd.h>
#define PATHD '/'

typedef pthread_t thread_t;
#define thread_create(thrp, attr, func, arg)                               /
   pthread_create((thrp), (attr), (func), (arg))
#define thread_join(thr, statusp) pthread_join((thr), (statusp))

typedef pthread_mutex_t mutex_t;
#define mutex_init(m, attr)     pthread_mutex_init((m), (attr))
#define mutex_lock(m)           pthread_mutex_lock(m)
#define mutex_unlock(m)         pthread_mutex_unlock(m)
#endif

int
main(int argc, char *argv[])
{
/*
* 打开dbenv使用的flags。注意这里我们要使用事务功能,所以需要加上
* DB_INIT_TXN | DB_INIT_LOG | DB_INIT_LOCK, 这三个宏都是必需的,
* 因为事务需要日志功能才能恢复,需要锁功能才能保证隔离性和数据一致性。
*/
   // Env open flags
   envFlags =
     DB_CREATE     | // Create the environment if it does not exist
     DB_RECOVER    | // Run normal recovery.
     DB_INIT_LOCK | // Initialize the locking subsystem
     DB_INIT_LOG   | // Initialize the logging subsystem
     DB_INIT_TXN   | // Initialize the transactional subsystem. This
                      // also turns on logging.
     DB_INIT_MPOOL | // Initialize the memory pool (in-memory cache)
     DB_THREAD;       // Cause the environment to be free-threaded

   try {
       // Create and open the environment
       envp = new DbEnv(0);

       // Indicate that we want db to internally perform deadlock
       // detection. Also indicate that the transaction with
       // the fewest number of write locks will receive the
       // deadlock notification in the event of a deadlock.
       envp->set_lk_detect(DB_LOCK_MINWRITE);
   /*
   * 上面的set_lk_detect调用的作用是在一个事务T请求锁一个页面P而被拒绝后,
   * 立刻进行死锁检查。这样做可以确保死锁尽早被发现并解除。但是很多时候这样
   * 做是多余的, 因为如果另一个事务T1正在锁着那个页面P,那么T的请求必然被拒绝,
   * 但是只要T等待一会儿,在T1把P的锁释放后,T就可以锁住P。这时并没有死锁,所做的
   * 死锁检测是徒劳的。不过对于一个很简单的不在意性能的程序来说,这样做
   * 也够了。
   *
   * 更加高效的死锁检测方法是在一个独立的线程中,定期单独调用DbEnv::lock_detect()函数
   * 来解除死锁。这个函数解除死锁的方式就是,让死锁等待环中的某个事务的锁请求失败,于是那个
   * 数据库操作函数,比如Db::get或者Db::put就有了一个DB_LOCK_DEADLOCK的错误返回,或者
   * 有一个DbDeadLockException异常从其中被抛出。收到这个错误或者异常后,应用程序必须abort
   * 当前的事务T0,然后,还可以可选地重新执行这个事务。T0被abort后,它持有的锁被释放,
   * 于是死锁的环路等待条件被破坏,于是剩余的事务可以拿到请求的锁而继续进行。
   * */

       envp->open(dbHomeDir, envFlags, 0);


       // If we had utility threads (for running checkpoints or
       // deadlock detection, for example) we would spawn those
       // here. However, for a simple example such as this,
       // that is not required.

       // Open the database
   // 打开一个数据库。该数据库支持自动提交事务,以及读取未提交数据。
   // 还支持重复的键值,并且在键值重复的时候,key/data pair的数据部分按顺序排列。
   // 这样做的好处是读操作速度很快。见后面的代码和注释。
   //
       openDb(&dbp, progName, fileName,
           envp, DB_DUPSORT);

       // Initialize a mutex. Used to help provide thread ids.
       (void)mutex_init(&thread_num_lock, NULL);

       // Start the writer threads. 创建子线程。
       for (i = 0; i < NUMWRITERS; i++)
           (void)thread_create(
               &writerThreads[i], NULL,
               writerThread, (void *)dbp);

       // Join the writers 等待所有子线程完成。
       for (i = 0; i < NUMWRITERS; i++)
           (void)thread_join(writerThreads[i], NULL);

   } catch(DbException &e) {
       std::cerr << "Error opening database environment: "
                 << dbHomeDir << std::endl;
       std::cerr << e.what() << std::endl;
       return (EXIT_FAILURE);
   }

   try {
       // Close our database handle if it was opened.
       if (dbp != NULL)
           dbp->close(0);

       // Close our environment if it was opened.
       if (envp != NULL)
           envp->close(0);
   } catch(DbException &e) {
       std::cerr << "Error closing database and environment."
                 << std::endl;
       std::cerr << e.what() << std::endl;
       return (EXIT_FAILURE);
   }

   // Final status message and return.

   std::cout << "I'm all done." << std::endl;
   return (EXIT_SUCCESS);
}

// A function that performs a series of writes to a
// Berkeley DB database. The information written
// to the database is largely nonsensical, but the
// mechanism of transactional commit/abort and
// deadlock detection is illustrated here.
void *
writerThread(void *args)
{
   int j, thread_num;
   int max_retries = 20;   // Max retry on a deadlock
   char *key_strings[] = {"key 1", "key 2", "key 3", "key 4",
                          "key 5", "key 6", "key 7", "key 8",
                          "key 9", "key 10"};

   Db *dbp = (Db *)args;
   DbEnv *envp = dbp->get_env();

   // Get the thread number
   (void)mutex_lock(&thread_num_lock);
   global_thread_num++;
   thread_num = global_thread_num;
   (void)mutex_unlock(&thread_num_lock);

   // Initialize the random number generator
   srand(thread_num);

   // Perform 50 transactions
   for (int i=0; i<50; i++) {
       DbTxn *txn;
       bool retry = true;
       int retry_count = 0;
       // while loop is used for deadlock retries
       while (retry) {
           // try block used for deadlock detection and
           // general db exception handling
           try {

               // Begin our transaction. We group multiple writes in
               // this thread under a single transaction so as to
               // (1) show that you can atomically perform multiple
               // writes at a time, and (2) to increase the chances
               // of a deadlock occurring so that we can observe our
               // deadlock detection at work.

               // Normally we would want to avoid the potential for
               // deadlocks, so for this workload the correct thing
               // would be to perform our puts with autocommit. But
               // that would excessively simplify our example, so we
               // do the "wrong" thing here instead.
               txn = NULL;

       // 启动一个事务。事务可以是嵌套的,这里我们没有用到嵌套事务。
       // 此处我们还可以设置事务的其他属性,在第三个参数中。事务的属性
       // 包括:
       // 1. 事务的隔离级别
       // 默认是Serialized,你可以设置为read uncommitted或者read committed.
       // Berkeley DB没有教科书中的单独的介于read committed和serialized级别
       // 之间的repeatable read级别。repeatable read与serialized之间的区别
       // 就是phantom现象,也就是在所有锁住的数据中,被插入了更多的数据。
       // 其原因是很多数据库做行级别的锁定,所以当你在一个select语句中把每
       // 一行都锁住后,其他事物仍然可以向数据库中插入更多符合条件的数据。
       // 于是在同一个事务中你再一次执行同样的select语句时候,得到的数据集与之前不同。
       // 这样做的好处是并行性比直接锁住行所在的页面这种办法更高一些。
       // 而Berkeley DB是基于页面来锁定的,任何一个key/data pair如果在一个事务中
       // 被访问过,那么我们对它所在的页面加锁,自然不会出现phantom现象。
       //
       // 借此机会顺便聊一下read committed与repeatable read直接的区别:
       // read committed级别中,数据库读操作锁住的行会在游标离开这行后释放。
       // 所以事务提交前如果先后多次访问同一行,读到的数据可能是不一样的,
       // 这个级别的一致性保证也叫做cursor stability。
       //
       // 这样我们可以看出,这四个隔离级别:
       // read uncommitted (ru), read committed(rc), repeatable read (rr), serialized (sa)
       // 中,隔离性逐渐提高,代价是并发性逐渐降低。
       // ru允许写操作在完成后立即释放锁,不等待事务完成;
       // rc要求写锁必须在事务完成时候是否,但是读锁可以在读操作完成后立即释放;
       // rr要求读写锁都必须在事务完成时候释放(2pl),但是只锁相关行;
       // sa在rr的要求基础上,锁住了上一层次的数据库对象(表或者页面),从而避免幻象(phantom)。
       // 应用程序应该根据其对数据一致性的要求,选择合适的隔离级别。
       //
       // 2. 事务的durability level(持久性级别)
       // 事务的持久性是通过在事务成功提交之前,把事物的日志写到磁盘上来保证的:
       // 任何提交成功的事务的日志都一定是存储在持久介质上的,所以数据库崩溃了,
       // 可以用这些日志来恢复。但是这样做意味着每次事务提交都有磁盘操作,性能自然
       // 有所影响。所以如果有其他方式保证事务的持久性,比如通过replication;或者
       // 应用程序不需要事务的持久性,那么可以设置更低级别的持久性保证,从而增加
       // 事务吞吐量。
       // 3. 使用MVCC。这个以后单独成篇介绍。
               envp->txn_begin(NULL, &txn, 0);

               // Perform the database write for this transaction.
               for (j = 0; j < 10; j++) {
                   Dbt key, value;
                   key.set_data(key_strings[j]);
                   key.set_size((u_int32_t)strlen(key_strings[j]) + 1);

                   int payload = rand() + i;
                   value.set_data(&payload);
                   value.set_size(sizeof(int));

                   // Perform the database put
           // 执行一个数据库更新操作。其中当前事务会请求锁住某个页面P。
           // 虽然我们打开的数据库支持自动提交(DB_AUTO_COMMIT),但是这里
           // 我们传入了已经启动的事务,所以,这个循环中所有的Db::put操作
           // 构成一个单一的事务,而不是每个Db::put自己构成一个事务。
                   dbp->put(txn, &key, &value, 0);
               }

               // countRecords runs a cursor over the entire database.
               // We do this to illustrate issues of deadlocking
       // 这里我们读取数据库中每一条记录,所以很容易发生死锁。
               std::cout << thread_num << " : Found "
                         << countRecords(dbp, NULL)
                         << " records in the database." << std::endl;

               std::cout << thread_num << " : committing txn : " << i
                         << std::endl;

               // commit
               try {
       // 提交当前事务。
                   txn->commit(0);
                   retry = false; // 事务提交成功,不需要重新执行该事务。
                   txn = NULL;
               } catch (DbException &e) {
                   std::cout << "Error on txn commit: "
                             << e.what() << std::endl;
               }
           } catch (DbDeadlockException &) {
       // 如前所述,数据库读写操作可能因为死锁而无法完成,异常抛出。这里的
       // 处理方式是固定的:中止事务。
               // First thing that we MUST do is abort the transaction.
               if (txn != NULL)
                   (void)txn->abort();

       // 可选地重新执行这个事务。
               // Now we decide if we want to retry the operation.
               // If we have retried less than max_retries,
               // increment the retry count and goto retry.
               if (retry_count < max_retries) {
                   std::cout << "############### Writer " << thread_num
                             << ": Got DB_LOCK_DEADLOCK./n"
                             << "Retrying write operation."
                             << std::endl;
                   retry_count++;
                   retry = true;// 死锁发生,重新执行该事务的次数为超过上限,所以重新执行。
                } else {
                   // Otherwise, just give up.
                   std::cerr << "Writer " << thread_num
                             << ": Got DeadLockException and out of "
                             << "retries. Giving up." << std::endl;
                   retry = false;
                }
          } catch (DbException &e) {
               std::cerr << "db put failed" << std::endl;
               std::cerr << e.what() << std::endl;
       // 有其他异常抛出,可能的原因有很多,但是首先要做的就是中止事务,
       // 因为那个数据库操作没有完成。
               if (txn != NULL)
                   txn->abort();
               retry = false;
          } catch (std::exception &ee) {
           std::cerr << "Unknown exception: " << ee.what() << std::endl;
           return (0);
         }
       }
   }
   return (0);
}


// This simply counts the number of records contained in the
// database and returns the result. You can use this method
// in three ways:
//
// First call it with an active txn handle.
// Secondly, configure the cursor for uncommitted reads
//
// Third, call countRecords AFTER the writer has committed
//    its transaction.
//
// If you do none of these things, the writer thread will
// self-deadlock.
//
// Note that this method exists only for illustrative purposes.
// A more straight-forward way to count the number of records in
// a database is to use the Database.getStats() method.
int
countRecords(Db *dbp, DbTxn *txn)
{

   Dbc *cursorp = NULL;
   int count = 0;

   try {
       // Get the cursor
   // 打开一个游标。传入txn参数使得这个打开的游标上的所有数据库操作处于事务txn中。
       dbp->cursor(txn, &cursorp, DB_READ_UNCOMMITTED);

       Dbt key, value;
   // 遍历数据库中每一个key/data pair.
       while (cursorp->get(&key, &value, DB_NEXT) == 0) {
           count++;
       }
   } catch (DbDeadlockException &de) {
   // 检查死锁异常。发现死锁后,此处要首先关闭游标。因为在abort或者commit一个
   // 事务的时候,不可以有打开的游标存在。关闭游标后才可以abort事务txn。不过这里我们
   // 直接重新抛出这个异常,以便该函数的调用者做进一步的处理,调用者会中止事务。
       std::cerr << "countRecords: got deadlock" << std::endl;
       cursorp->close();
       throw de;
   } catch (DbException &e) {
       std::cerr << "countRecords error:" << std::endl;
       std::cerr << e.what() << std::endl;
   }

   if (cursorp != NULL) {
       try {
           cursorp->close();
       } catch (DbException &e) {
           std::cerr << "countRecords: cursor close failed:" << std::endl;
           std::cerr << e.what() << std::endl;
       }
   }

   return (count);
}


// Open a Berkeley DB database
int
openDb(Db **dbpp, const char *progname, const char *fileName,
DbEnv *envp, u_int32_t extraFlags)
{
   int ret;
   u_int32_t openFlags;

   try {
       Db *dbp = new Db(envp, 0);

       // Point to the new'd Db
       *dbpp = dbp;

       if (extraFlags != 0)
           ret = dbp->set_flags(extraFlags);

       // Now open the database */
   // 这里要注意的是:
   // 1. 这个数据库的隔离级别是 read uncommitted, 也就是在每个Db::put调用操作返回之前,
   // 该操作获取的锁就被释放了,而不是在事务提交/终止之前释放(2阶段锁 2PL locking)。
   // 这样做的好处是数据库读/写操作可以最大化地并行执行。
   // 缺点是读到的有可能是没有提交的数据,即脏数据。并且发生死锁的可能性更大,
   // 不过这正是这个例子程序需要的。
   //
   // 2. 每一个数据库操作函数是自动提交的,也就是每个数据库函数是一个独立的事务,
   // 除非它已经在一个外部事务中。
       openFlags = DB_CREATE              | // Allow database creation
                   DB_READ_UNCOMMITTED    | // Allow uncommitted reads
                   DB_AUTO_COMMIT;          // Allow autocommit

       dbp->open(NULL,       // Txn pointer
                 fileName,   // File name
                 NULL,       // Logical db name
                 DB_BTREE,   // Database type (using btree)
                 openFlags, // Open flags
                 0);         // File mode. Using defaults
   } catch (DbException &e) {
       std::cerr << progname << ": openDb: db open failed:" << std::endl;
       std::cerr << e.what() << std::endl;
       return (EXIT_FAILURE);
   }

   return (EXIT_SUCCESS);
}

你可能感兴趣的:(DB,database,Berkeley,关系数据库)