使用acl網絡通訊庫的 redis c++ 模塊開發 redis 應用

 1、概述php

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

      在後來的屢次項目開發中被官方的 hiredis 庫多次摧殘後,終於忍受不了,決定從新開發一套全新的 redis 客戶端 API,該庫不只要實現這 150 多個客戶端命令,同時須要提供方便靈活的鏈接池及鏈接池集羣管理功能(幸運的是在 acl 庫中已經具有了通用的網絡鏈接池及鏈接池集羣管理模塊),另外,根據以前的實踐,有可能提供的函數接口要遠大於這 150 多個,緣由是針對同一個命令可能會由於不一樣的參數類型場景提供多個函數接口(最終的結果是提供了3,4百個函數 API);在仔細研究了 redis 的通訊協議後便着手開始進行設計開發了(redis 的協議設計仍是很是簡單實用的,即能支持二進制,同時又便於手工調試)。在開發過程當中大量參考了 http://redisdoc.com 網站上的中文在線翻譯版(很是感謝 黃鍵宏 同窗的辛勤工做)。git

 2、acl redis 庫分類程序員

      根據 redis 的數據結構類型,分紅 12 個大類,每一個大類提供不一樣的函數接口,這 12 個 C++ 類展現以下:github

      一、redis_key:redis 全部數據類型的統一鍵操做類;由於 redis 的數據結構類型都是基本的 KEY-VALUE 類型,其中 VALUE 分爲不一樣的數據結構類型;redis

      二、redis_connectioin:與 redis-server 鏈接相關的類;算法

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

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

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

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

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

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

      九、redis_pubsub:redis 中用來表示「發佈-訂閱」的數據類型;

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

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

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

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

       1三、redis_command:以上 12 個類的基類;

       1四、redis_client:redis 客戶端網絡鏈接類;

       1五、redis_result:redis 命令結果類;

       1六、redis_pool:針對以上全部命令支持鏈接池方式;

       1七、redis_manager:針對以上全部命令容許與多個 redis-server 服務創建鏈接池集羣(即與每一個 redis-server 創建一個鏈接池)。

 

3、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 操做類對象綁定後進行操做。

 4、小結

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

 源碼下載:

國內:http://git.oschina.net/zsxxsz/acl/tree/master

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

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

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

 BBS:http://www.acl-dev.com/

QQ羣:242722074

相關文章
相關標籤/搜索