使用 acl 庫編寫高效的 C++ redis 客戶端應用

jopen 9年前發布 | 43K 次閱讀 Redis C/C++開發

一、概述

(可以直接略過此段)redis 最近做為 nosql 數據服務應用越來越廣泛,其相對于 memcached 的最大優點是提供了更加豐富的數據結構,所以應用場景就更為廣泛。redis 的出現可謂是廣大網絡應用開發者的福音,同時有大量的開源人員貢獻了客戶端代碼,象針對 java 語言的 jedis,php 語言的 phpredis/predis 等,這些語言的 redis 庫既豐富又好用,而對 C/C++ 程序員似乎就沒那么幸運了,官方提供了 C 版的 hiredis 作為客戶端庫,很多愛好者都是基于 hiredis 進行二次封裝和開發形成了 C++ 客戶端庫,但這些庫(包括官方的 hiredis)大都使用麻煩,給使用者造成了許多出錯的機會。一直想開發一個更易用的接口型的 C++ 版 redis 客戶端庫(注:官方提供的庫基本屬于協議型的,這意味著使用者需要花費很多精力去填充各個協議字段同時還得要分析服務器可能返回的不同的結果類型),但每當看到 redis 那 150 多個客戶端命令時便心生退縮,因為要給每個命令提供一個方便易用的 C++ 函數接口,則意味著非常巨大的開發工作量。

在后來的多次項目開發中被官方的 hiredis 庫屢次摧殘后,終于忍受不了,決定重新開發一套全新的 redis 客戶端 API,該庫不僅要實現這 150 多個客戶端命令,同時需要提供方便靈活的連接池及連接池集群管理功能(幸運的是在 acl 庫中已經具備了通用的網絡連接池及連接池集群管理模塊),另外,根據之前的實踐,有可能提供的函數接口要遠大于這 150 多個,原因是針對同一個命令可能會因為不同的參數類型場景提供多個函數接口(最終的結果是提供了3,4百個函數 API);在仔細研究了 redis 的通信協議后便著手開始進行設計開發了(redis 的協議設計還是非常簡單實用的,即能支持二進制,同時又便于手工調試)。在開發過程中大量參考了 http://redisdoc.com 網站上的中文在線翻譯版(非常感謝 黃鍵宏 同學的辛勤工作)。

二、acl redis 庫分類

根據 redis 的數據結構類型,分成 12 個大類,每個大類提供不同的函數接口,這 12 個 C++ 類展示如下:

1、redis_key:redis 所有數據類型的統一鍵操作類;因為 redis 的數據結構類型都是基本的 KEY-VALUE 類型,其中 VALUE 分為不同的數據結構類型;

2、redis_connectioin:與 redis-server 連接相關的類;

3、redis_server:與 redis-server 服務管理相關的類;

4、redis_string:redis 中用來表示字符串的數據類型;

5、redis_hash:redis 中用來表示哈希表的數據類型;每一個數據對象由 “KEY-域值對集合” 組成,即一個 KEY 對應多個“域值對”,每個“域值對”由一個字段名與字段值組成;

6、redis_list:redis 中用來表示列表的數據類型;

7、redis_set:redis 中用來表示集合的數據類型;

8、redis_zset:redis 中用來表示有序集合的數據類型;

9、redis_pubsub:redis 中用來表示“發布-訂閱”的數據類型;

10、redis_hyperloglog:redis 中用來表示 hyperloglog 基數估值算法的數據類型;

11、redis_script:redis 中用來與 lua 腳本進行轉換交互的數據類型;

12、redis_transaction:redis 中用以事務方式執行多條 redis 命令的數據類型(注:該事務處理方式與數據庫的事務有很大不同,redis 中的事務處理過程沒有數據庫中的事務回滾機制,僅能保證其中的多條命令都被執行或都不被執行);

除了以上對應于官方 redis 命令的 12 個類別外,在 acl 庫中還提供了另外幾個類:

13、redis_command:以上 12 個類的基類;

14、redis_client:redis 客戶端網絡連接類;

15、redis_result:redis 命令結果類;

16、redis_pool:針對以上所有命令支持連接池方式;

17、redis_manager:針對以上所有命令允許與多個 redis-server 服務建立連接池集群(即與每個 redis-server 建立一個連接池)。

三、acl redis 使用舉例

1)、下面是一個使用 acl 框架中 redis 客戶端庫的簡單例子:

/**
 * @param conn {acl::redis_client&} redis 連接對象
 * @return {bool} 操作過程是否成功
 */
bool test_redis_string(acl::redis_client& conn, const char* key)
{
    // 創建 redis string 類型的命令操作類對象,同時將連接類對象與操作類
    // 對象進行綁定
    acl::redis_string string_operation(&conn);
    const char* value = "test_value";

    // 添加 K-V 值至 redis-server 中
    if (string_operation.set(key, value) == false)
    {
        const acl::redis_result* res = string_operation.get_result();
        printf("set key: %s error: %s\r\n",
            key, res ? res->get_error() : "unknown error");
        return false;
    }
    printf("set key: %s ok!\r\n", key);

    // 需要重置連接對象的狀態,或直接調用 conn.reset() 也可
    string_operation.reset();

    // 從 redis-server 中取得對應 key 的值
    acl::string buf;
    if (string_operation.get(key, buf) == false)
    {
        const acl::redis_result* res = string_operation.get_result();
        printf("get key: %s error: %s\r\n",
            key, res ? res->get_error() : "unknown error");
        return false;
    }
    printf("get key: %s ok, value: %s\r\n", key, buf.c_str());

    // 探測給定 key 是否存在于 redis-server 中,需要創建 redis 的 key
    // 類對象,同時將 redis 連接對象與之綁定
    acl::redis_key key_operation;
    conn.reset(); // 重置連接狀態
    key_operation.set_client(conn);  // 將連接對象與操作對象進行綁定
    if (key_operation.exists(key) == false)
    {
        if (conn.eof())
        {
            printf("disconnected from redis-server\r\n");
            return false;
        }

        printf("key: %s not exists\r\n", key);
    }
    else
        printf("key: %s exists\r\n", key);

    // 刪除指定 key 的字符串類對象
    conn.reset(); // 先重置連接對象狀態
    if (key_operation.del(key, NULL) < 0)
    {
        printf("del key: %s error\r\n", key);
        return false;
    }
    else
        printf("del key: %s ok\r\n", key);

    return true;
}

/**
 * @param redis_addr {const char*} redis-server 服務器地址,
 *  格式為:ip:port,如:127.0.0.1:6379
 * @param conn_timeout {int} 連接 redis-server 的超時時間(秒)
 * @param rw_timeout {int} 與 redis-server 進行通信的 IO 超時時間(秒)
 */
bool test_redis(const char* redis_addr, int conn_timeout, int rw_timeout)
{
    // 創建 redis 客戶端網絡連接類對象
    acl::redis_client conn(redis_addr, conn_timeout, rw_timeout);
    const char* key = "test_key";
    return test_redis_string(conn, key);
}

上面的簡單例子的操作過程是:在 redis-server 中添加字符串類型數據 --> 從 redis-server 中獲取指定的字符串數據 --> 判斷指定指定 key 的對象在 redis-server 上是否存在 ---> 從 redis-server 中刪除指定 key 的數據對象(即該例中的字符串對象)。通過以上簡單示例,使用者需要注意以下幾點:

a)、acl 中的 redis 庫的設計中 redis 連接類對象與命令操作類對象是分離的,12 個 redis 命令操作類對應 acl redis 庫中相應的 12 個命令操作類;

b)、在使用 redis 命令操作類時需要先將 redis 連接類對象與命令操作類對象進行綁定(以便于操作類內部可以利連接類中的網絡連接、協議組包以及協議解析等方法);

c)、在重復使用一個 redis 連接類對象時,需要首先重置該連接類對象的狀態(即調用:acl::redis_client::reset()),這樣主要是為了釋放上一次命令操作過程的中間內存資源;

d)、一個 redis 連接類對象可以被多個命令類操作類對象使用(使用前需先綁定一次);

e)、將 redis 連接對象與命令操作對象綁定有兩種方式:可以在構造函數中傳入非空 redis 連接對象,或調用操作對象的 set_client 方法進行綁定。

2)、對上面的例子稍加修改,使之能夠支持連接池方式,示例代碼如下:

/**
 * @param conn {acl::redis_client&} redis 連接對象
 * @return {bool} 操作過程是否成功
 */
bool test_redis_string(acl::redis_client& conn, const char* key)
{
    ...... // 代碼與上述代碼相同,省略

    return true;
}

// 子線程處理類
class test_thread : public acl::thread
{
public:
    test_thread(acl::redis_pool& pool) : pool_(pool) {}

    ~test_thread() {}

protected:
    // 基類(acl::thread)純虛函數
    virtual void* run()
    {
        acl::string key;
        // 給每個線程一個自己的 key,以便以測試,其中 thread_id()
        // 函數是基類 acl::thread 的方法,用來獲取線程唯一 ID 號
        key.format("test_key: %lu", thread_id());

        acl::redis_client* conn;

        for (int i = 0; i < 1000; i++)
        {
            // 從 redis 客戶端連接池中獲取一個 redis 連接對象
            conn = (acl::redis_client*) pool_.peek();
            if (conn == NULL)
            {
                printf("peek redis connection error\r\n");
                break;
            }

            // 進行 redis 客戶端命令操作過程
            if (test_redis_string(*conn) == false)
            {
                printf("redis operation error\r\n");
                break;
            }
        }

        return NULL;
    }

private:
    acl::redis_pool& pool_;
};

void test_redis_pool(const char* redis_addr, int max_threads,
    int conn_timeout, int rw_timeout)
{
    // 創建 redis 連接池對象
    acl::redis_pool pool(redis_addr, max_threads);
    // 設置連接 redis 的超時時間及 IO 超時時間,單位都是秒
    pool.set_timeout(conn_timeout, rw_timeout);

    // 創建一組子線程
    std::vector<test_thread*> threads;
    for (int i = 0; i < max_threads; i++)
    {
        test_thread* thread = new test_thread(pool);
        threads.push_back(thread);
        thread->set_detachable(false);
        thread->start();
    }

    // 等待所有子線程正常退出
    std::vector<test_thread*>::iterator it = threads.begin();
    for (; it != threads.end(); ++it)
    {
        (*it)->wait();
        delete (*it);
    }
}

除了創建線程及 redis 連接池外,上面的例子與示例 1) 的代碼與功能無異。

3)、下面對上面的示例2)稍作修改,使之可以支持 redis 集群連接池的方式,示例代碼如下:

/**
 * @param conn {acl::redis_client&} redis 連接對象
 * @return {bool} 操作過程是否成功
 */
bool test_redis_string(acl::redis_client& conn, const char* key)
{
    ......  // 與上面示例代碼相同,略去
    return true;
}

// 子線程處理類
class test_thread : public acl::thread
{
public:
    test_thread(acl::redis_manager& manager) : manager_(manager) {}

    ~test_thread() {}

protected:
    // 基類(acl::thread)純虛函數
    virtual void* run()
    {
        acl::string key;
        // 給每個線程一個自己的 key,以便以測試,其中 thread_id()
        // 函數是基類 acl::thread 的方法,用來獲取線程唯一 ID 號
        key.format("test_key: %lu", thread_id());

        acl::redis_pool* pool;
        acl::redis_client* conn;

        for (int i = 0; i < 1000; i++)
        {
            // 從連接池集群管理器中獲得一個 redis-server 的連接池對象
            pool = (acl::redis_pool*) manager_.peek();
            if (pool == NULL)
            {
                printf("peek connection pool failed\r\n");
                break;
            }

            // 從 redis 客戶端連接池中獲取一個 redis 連接對象
            conn = (acl::redis_client*) pool_.peek();
            if (conn == NULL)
            {
                printf("peek redis connection error\r\n");
                break;
            }

            // 進行 redis 客戶端命令操作過程
            if (test_redis_string(*conn) == false)
            {
                printf("redis operation error\r\n");
                break;
            }
        }

        return NULL;
    }

private:
    (acl::redis_manager& manager_;
};

void test_redis_pool(const char* redis_addr, int max_threads,
    int conn_timeout, int rw_timeout)
{
    // 創建 redis 集群連接池對象
    acl::redis_manager manager(conn_timeout, rw_timeout);

    // 添加多個 redis-server 的服務器實例地址
    manager.set("127.0.0.1:6379", max_threads);
    manager.set("127.0.0.1:6380", max_threads);
    manager.set("127.0.0.1:6381", max_threads);

    // 設置連接 redis 的超時時間及 IO 超時時間,單位都是秒
    pool.set_timeout(conn_timeout, rw_timeout);

    // 創建一組子線程
    std::vector<test_thread*> threads;
    for (int i = 0; i < max_threads; i++)
    {
        test_thread* thread = new test_thread(manager);
        threads.push_back(thread);
        thread->set_detachable(false);
        thread->start();
    }

    // 等待所有子線程正常退出
    std::vector<test_thread*>::iterator it = threads.begin();
    for (; it != threads.end(); ++it)
    {
        (*it)->wait();
        delete (*it);
    }
}

該示例只修改了幾處代碼便支持了集群 redis 連接池方式,其處理過程是:創建集群連接池對象(可以添加多個 redis-server 服務地址) --> 從集群連接池對象中取得一個連接池對象 ---> 從該連接池對象中取得一個連接 ---> 該連接對象與 redis 操作類對象綁定后進行操作。

四、小結

以上介紹了 acl 框架中新增加的 redis 庫的使用方法及處理過程,該庫將復雜的協議及網絡處理過程隱藏在實現內部,使用戶使用起來感覺象是在調用本的函數。在示例 2)、3) 中提到了 acl 線程的使用,有關 acl 庫中更為詳細地使用線程的文章參見:《使用 acl_cpp 庫編寫多線程程序》

下載:http://sourceforge.net/projects/acl/

svn:svn://svn.code.sf.net/p/acl/code/trunk

github:https://github.com/zhengshuxin/acl

來自:http://zsxxsz.iteye.com/blog/2184744

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!