基於Netty的Redis客戶端-Nedis

最近溫習了一遍Redis命令,憂傷的是很多東西已交還給老師,正好趕上antirez大神在愚人節發佈了Redis 3.0,Redis終於有了支持集羣的正式版本,於是心血來潮決定自己實現一個Redis客戶端來撫慰我這顆憂傷的心靈。

Jedis已經足夠強大,它的網絡連接是基於阻塞式IO,實現非常簡單易懂,但是OIO和NIO相比性能上有劣勢,於是決定通過NIO來實現和Redis服務器的網絡連接,現在業界最優秀的NIO框架非Netty莫屬了,正好以前也學過Netty框架,所以決定基於Netty來實現這個Redis客戶端,這樣還可以同時再次熟悉一下Netty,於是一個高大上的名字新鮮出爐-Nedis。關於命令的實現就沒什麼好糾結,完全參照Redis官方文檔來就可以了,也可以參考Jedis代碼。

由於本碼農平時工作比較忙,在公司工作時是不可能抽時間來搞的,一沒時間,大佬們各種催活,二是由於公司的信息安全政策,公司裏面寫的代碼是拿不出來的。所以只有利用晚上下班時間和週末的業餘時間來搞,工作日有2-3個小時的時間,大概10點開搞,到1點左右,週末由於要帶娃做飯,也只能擠出3-4個小時出來,所以進展比較慢,從4月初到現在將近20天的時間終於完成了key、string、hash、list、set、 SortedSet的所有單機命令以及客戶端分片(Sharding),其它的事務、lua腳本、集羣等功能還未實現,留到後面版本再實現。

框架組件

首先最優先的是要確定代碼的基本骨架,骨架確定之後各個命令的實現就純粹是工作量上的事了,事實上這種工作非常的機械。經過多番的修改重構最終確定了代碼骨架,命令的基本流程,下面是Nedis中幾個基礎的組件:

  • NedisClient和ShardedNedis:該框架中的兩個門面,分別處理單機命令和客戶端key分片,對使用提供了所有命令的調用接口,由於事務和集羣等功能還沒有提供,所有未來可能會增加TransationNedis和ClusterNedis等。所有命令最終都會經過Nedis轉發,SharedNedis最終也會調用NedisClient。NedisClient的構建採用build模式,通過NedisClientBuilder來構建,關於客戶端參數,目前設計得比較粗糙,參數較少,目前可以給NedisClient設置如下參數:

參數 左右
connectTimeoutMills 連接超時時間
eventLoopGroupSize Netty框架的線程池大小
tcpNoDelay 是否設置TCP_NO_DELAY標識
connectionPoolSize 連接池大小
maxConnectionIdleTimeInMills 空閒連接超時時間
minIdleConnections 最小空閒連接數量

  • ConnectionPool:爲了減少重複建連開銷,採用連接池複用連接,命令最終通過連接池中的連接發送,通過IdleStateHandler監控空閒的連接。
  • ProtocolDecoder:讀取命令響應,對響應進行解析,繼承自ReplayingDecoder處理分包傳輸,命令響應的解析最終委託給RedisProtocol進行處理。
  • ResponseReceiver:接收ProtocolDecoder的響應解析結果,把結果交給ResponeAdapter進行適配,並把最終適配後的結果通過ResponseCallback回調返回給命令調用者,處理完成之後最終把連接返回到連接池。
  • ResponeAdapter:把ProtocolDecoder解析出來的結果適配成對使用者比較友好的類型。
  • ResponseCallback:因爲Netty是一個異步處理框架,所以我們提供的命令接口不會阻塞直到命令返回(這樣就無法體現NIO和Netty的優勢了),而是在命令響應達到時通過ResponseCallback回調通知調用者。

下面是命令發送和響應的時序:

Nedis命令流程

測試

就目前已完成的代碼來講,Nedis中的單測代碼遠遠超出了框架代碼,目前的單測代碼8000行左右而框架代碼只有4000左右,而且最關鍵的是由於時間有限,測試代碼並沒有覆蓋所有路徑,有些命令特別是分片相關的命令接口還沒寫單測(那些沒完成的單元測試代碼只有等後面慢慢補上),所以覆蓋率估計50%都不到,也就是說正常的比例單元測試代碼:框架代碼應該大於4:1,是不是很震驚,但是測試代碼是必須要寫的,這是一件一勞永逸的事兒,有了單測代碼後面改代碼時很容易驗證修改的代碼有沒有問題會不會引發其它問題,當然前提是測試代碼要可靠。

對我們的命令測試,有兩個問題:

  • 在測試命令時可能要依賴其它命令提供數據,比如我想測一個get命令,那麼在之前需要通過set命令構造數據來支持get命令的測試,這就要求在get命令執行之前必須得先執行set命令,但是由於Netty框架通過線程池來執行任務,set命令和get命令可能會由不同的線程來執行,這樣的話命令執行可能會亂序,即使set命令再get命令之前調用,也不能完全保證set命令先到達服務器,所有在調用set命令接口需要做一個小停頓再調用get命令接口,來保證set命令在get命令之前執行,這個停頓時間一般100ms就夠了(我設的是200ms)。
  • 因爲Netty框架是異步的,所有調用命令接口時不會發生阻塞,所以爲了驗證測試效果,需要保證單測方法在響應回來之前不會結束,我們通過一個同步控制器CountDownLatch,在方法結束前調用await方法進行阻塞,在最後一個響應回來時調用countDown釋放,嗯,灰常不優雅。

下面是一個測試方法,controller就是上面說的CountDownLatch,CMD_PAUSE_TIME是停頓時間,它是一個放在基類中的常量,可以隨便修改,修改之後所有的測試方法都會生效:

@Test
public void testSet() {

	doCmdTest(new TestAction() {

		@Override
		public void doTest() throws InterruptedException, NedisException {
			client.flushAll(null);
			Thread.sleep(CMD_PAUSE_TIME);
			client.set(new ResponseCallback<String>() {

				@Override
				public void done(String result) {
					assertEquals("OK", result);
				}

				@Override
				public void failed(Throwable cause) {
					fail(cause);
				}
			}, "key1", "value1");

			Thread.sleep(CMD_PAUSE_TIME);
			client.set(new ResponseCallback<String>() {

				@Override
				public void done(String result) {
					assertEquals("OK", result);
					controller.countDown();
				}

				@Override
				public void failed(Throwable cause) {
					fail(cause);
					controller.countDown();
				}
			}, "key1", "value2");

		}
	});

}

分享

本着人人爲我我爲人人,框架代碼已經上傳到github:https://github.com/chenyihan/nedis,代碼需要有JDK7進行編譯,接下來還會實現剩餘的功能。非常慚愧從github上搞到過很多寶貴的資源,但是迄今爲止只共享過兩份代碼,以後一定要爲github多多貢獻,大家共同進步。

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