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);
}
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章