《Redis开发与运维》- 核心知识整理二(Lua脚本、发布订阅、客户端等)
目錄
- 1 事物
- 2 Lua腳本
- 2.1 Lua腳本的好處
- 2.2 Lua腳本的使用
- 2.3 script kill
- 3 Bitmaps
- 3.1 數據結構模型
- 3.2 Bitmaps的指令
- 3.3 Bitmaps分析
- 4 發布訂閱
- 4.1 基本概念
- 4.2 命令
- 4.3 使用場景
- 5 客戶端通信協議
- 6 Java客戶端Jedis
- 6.1 Jedis的基本使用方法
- 6.2 Jedis連接池的使用方法
- 7 客戶端API
- 7.1 client list
- 7.2 monitor
- 7.3 客戶端相關配置
1 事物
Redis提供了簡單的事務功能,將一組需要一起執行的命令放到multi和exec兩個命令之間。multi命令代表事務開始,exec命令代表事務結束,它們之間的命令是原子順序執行的,例如下面操作實現了上述用戶關注問題。
127.0.0.1:6379> multi OK 127.0.0.1:6379> sadd user:a:follow user:b QUEUED 127.0.0.1:6379> sadd user:b:fans user:a QUEUED可以看到sadd命令此時的返回結果是QUEUED,代表命令并沒有真正執行,而是暫時保存在Redis中。如果此時另一個客戶端執行sismember user:a:follow user:b返回結果應該為0。
127.0.0.1:6379> sismember user:a:follow user:b (integer) 0只有當exec執行后,用戶A關注用戶B的行為才算完成,如下所示返回的兩個結果對應sadd命令。
127.0.0.1:6379> exec 1) (integer) 1 2) (integer) 1 127.0.0.1:6379> sismember user:a:follow user:b (integer) 1如果要停止事務的執行,可以使用discard命令代替exec命令即可。
127.0.0.1:6379> discard OK 127.0.0.1:6379> sismember user:a:follow user:b (integer) 0如果事務中的命令出現錯誤,Redis的處理機制也不盡相同。
1.命令錯誤
例如下面操作錯將set寫成了sett,屬于語法錯誤,會造成整個事務無法執行,key和counter的值未發生變化:
127.0.0.1:6388> mget key counter 1) "hello" 2) "100" 127.0.0.1:6388> multi OK 127.0.0.1:6388> sett key world (error) ERR unknown command 'sett' 127.0.0.1:6388> incr counter QUEUED 127.0.0.1:6388> exec (error) EXECABORT Transaction discarded because of previous errors. 127.0.0.1:6388> mget key counter 1) "hello" 2) "100"2.運行時錯誤
例如用戶B在添加粉絲列表時,誤把sadd命令寫成了zadd命令,這種就是運行時命令,因為語法是正確的:
127.0.0.1:6379> multi OK 127.0.0.1:6379> sadd user:a:follow user:b QUEUED 127.0.0.1:6379> zadd user:b:fans 1 user:a QUEUED 127.0.0.1:6379> exec 1) (integer) 1 2) (error) WRONGTYPE Operation against a key holding the wrong kind of value 127.0.0.1:6379> sismember user:a:follow user:b (integer) 1可以看到Redis并不支持回滾功能,sadd user:a:follow user:b命令已經執行成功,開發人員需要自己修復這類問題。 有些應用場景需要在事務之前,確保事務中的key沒有被其他客戶端修改過,才執行事務,否則不執行(類似樂觀鎖)。Redis提供了watch命令來解決這類問題,下表展示了兩個客戶端執行命令的時序。
事務中watch命令演示時序
可以看到“客戶端-1”在執行multi之前執行了watch命令,“客戶端-2”在“客戶端-1”執行exec之前修改了key值,造成事務沒有執行(exec結果為nil),整個代碼如下所示:
#T1:客戶端1 127.0.0.1:6379> set key "java" OK #T2:客戶端1 127.0.0.1:6379> watch key OK #T3:客戶端1 127.0.0.1:6379> multi OK #T4:客戶端2 127.0.0.1:6379> append key python (integer) 11 #T5:客戶端1 127.0.0.1:6379> append key jedis QUEUED #T6:客戶端1 127.0.0.1:6379> exec (nil) #T7:客戶端1 127.0.0.1:6379> get key "javapython"Redis提供了簡單的事務,之所以說它簡單,主要是因為它不支持事務中的回滾特性,同時無法實現命令之間的邏輯關系計算,當然也體現了Redis的“keep it simple”的特性,Lua腳本同樣可以實現事務的相關功能,但是功能要強大很多。
2 Lua腳本
2.1 Lua腳本的好處
Lua腳本功能為Redis開發和運維人員帶來如下三個好處:
·Lua腳本在Redis中是原子執行的,執行過程中間不會插入其他命令。
·Lua腳本可以幫助開發和運維人員創造出自己定制的命令,并可以將這些命令常駐在Redis內存中,實現復用的效果。
·Lua腳本可以將多條命令一次性打包,有效地減少網絡開銷。
2.2 Lua腳本的使用
下面以一個例子說明Lua腳本的使用,當前列表記錄著熱門用戶的id,假設這個列表有5個元素,如下所示:
127.0.0.1:6379> lrange hot:user:list 0 -1 1) "user:1:ratio" 2) "user:8:ratio" 3) "user:3:ratio" 4) "user:99:ratio" 5) "user:72:ratio"user:{id}:ratio代表用戶的熱度,它本身又是一個字符串類型的鍵:
127.0.0.1:6379> mget user:1:ratio user:8:ratio user:3:ratio user:99:ratio user:72:ratio 1) "986" 2) "762" 3) "556" 4) "400" 5) "101"現要求將列表內所有的鍵對應熱度做加1操作,并且保證是原子執行,此功能可以利用Lua腳本來實現。
1)將列表中所有元素取出,賦值給mylist:
local mylist = redis.call("lrange", KEYS[1], 0, -1)2)定義局部變量count=0,這個count就是最后incr的總次數:
local count = 03)遍歷mylist中所有元素,每次做完count自增,最后返回count:
for index,key in ipairs(mylist) do redis.call("incr",key) count = count + 1 end return count將上述腳本寫入lrange_and_mincr.lua文件中,并執行如下操作,返回結果為5。
redis-cli --eval lrange_and_mincr.lua hot:user:list (integer) 5執行后所有用戶的熱度自增1:
127.0.0.1:6379> mget user:1:ratio user:8:ratio user:3:ratio user:99:ratio user:72:ratio 1) "987" 2) "763" 3) "557" 4) "401" 5) "102"本節給出的只是一個簡單的例子,在實際開發中,開發人員可以發揮自己的想象力創造出更多新的命令。
2.3 script kill
此命令用于殺掉正在執行的Lua腳本。如果Lua腳本比較耗時,甚至Lua腳本存在問題,那么此時Lua腳本的執行會阻塞Redis,直到腳本執行完畢或者外部進行干預將其結束。下面我們模擬一個Lua腳本阻塞的情況進行說明。下面的代碼會使Lua進入死循環:
while 1 == 1 do end執行Lua腳本,當前客戶端會阻塞:
127.0.0.1:6379> eval 'while 1==1 do end' 0Redis提供了一個lua-time-limit參數,默認是5秒,它是Lua腳本的“超時時間”,但這個超時時間僅僅是當Lua腳本時間超過lua-time-limit后,向其他命令調用發送BUSY的信號,但是并不會停止掉服務端和客戶端的腳本執行,所以當達到lua-time-limit值之后,其他客戶端在執行正常的命令時,將會收到“Busy Redis is busy running a script”錯誤,并且提示使用script kill或shutdown nosave命令來殺掉這個busy的腳本:
127.0.0.1:6379> get hello (error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.此時Redis已經阻塞,無法處理正常的調用,這時可以選擇繼續等待,但更多時候需要快速將腳本殺掉。使用shutdown save顯然不太合適,所以選擇script kill,當script kill執行之后,客戶端調用會恢復:
127.0.0.1:6379> script kill OK 127.0.0.1:6379> get hello "world"但是有一點需要注意,如果當前Lua腳本正在執行寫操作,那么script kill將不會生效。例如,我們模擬一個不停的寫操作:
while 1==1 do redis.call("set","k","v") end此時如果執行script kill,會收到如下異常信息:
(error) UNKILLABLE Sorry the script already executed write commands against the dataset. You can either wait the script termination or kill the server in a hard way using the SHUTDOWN NOSAVE command.上面提示Lua腳本正在向Redis執行寫命令,要么等待腳本執行結束要么使用shutdown save停掉Redis服務。可見Lua腳本雖然好用,但是使用不當破壞性也是難以想象的。
3 Bitmaps
3.1 數據結構模型
許多開發語言都提供了操作位的功能,合理地使用位能夠有效地提高內存使用率和開發效率。Redis提供了Bitmaps這個“數據結構”可以實現對位的操作。把數據結構加上引號主要因為:
·Bitmaps本身不是一種數據結構,實際上它就是字符串(如下圖所示),但是它可以對字符串的位進行操作。
·Bitmaps單獨提供了一套命令,所以在Redis中使用Bitmaps和使用字符串的方法不太相同。可以把Bitmaps想象成一個以位為單位的數組,數組的每個單元只能存儲0和1,數組的下標在Bitmaps中叫做偏移量。
字符串"big"用二進制表示
3.2 Bitmaps的指令
本節將每個獨立用戶是否訪問過網站存放在Bitmaps中,將訪問的用戶記做1,沒有訪問的用戶記做0,用偏移量作為用戶的id。
1.設置值
setbit key offset value設置鍵的第offset個位的值(從0算起),假設現在有20個用戶, userid=0,5,11,15,19的用戶對網站進行了訪問,那么當前Bitmaps初始化結果如圖所示。
setbit使用
具體操作過程如下,unique:users:2016-04-05代表2016-04-05這天的獨立訪問用戶的Bitmaps:
127.0.0.1:6379> setbit unique:users:2016-04-05 0 1 (integer) 0 127.0.0.1:6379> setbit unique:users:2016-04-05 5 1 (integer) 0 127.0.0.1:6379> setbit unique:users:2016-04-05 11 1 (integer) 0 127.0.0.1:6379> setbit unique:users:2016-04-05 15 1 (integer) 0 127.0.0.1:6379> setbit unique:users:2016-04-05 19 1 (integer) 0如果此時有一個userid=50的用戶訪問了網站,那么Bitmaps的結構變成了下圖所示,第20位~49位都是0。
userid=50用戶訪問
很多應用的用戶id以一個指定數字(例如10000)開頭,直接將用戶id和Bitmaps的偏移量對應勢必會造成一定的浪費,通常的做法是每次做setbit操作時將用戶id減去這個指定數字。在第一次初始化Bitmaps時,假如偏移量非常大,那么整個初始化過程執行會比較慢,可能會造成Redis的阻塞。
2.獲取值
getbit key offset獲取鍵的第offset位的值(從0開始算),下面操作獲取id=8的用戶是否在2016-04-05這天訪問過,返回0說明沒有訪問過:
127.0.0.1:6379> getbit unique:users:2016-04-05 8 (integer) 0由于offset=1000000根本就不存在,所以返回結果也是0:
127.0.0.1:6379> getbit unique:users:2016-04-05 1000000 (integer) 03.獲取Bitmaps指定范圍值為1的個數
bitcount [start][end]下面操作計算2016-04-05這天的獨立訪問用戶數量:
127.0.0.1:6379> bitcount unique:users:2016-04-05 (integer) 5[start]和[end]代表起始和結束字節數,下面操作計算用戶id在第1個字節到第3個字節之間的獨立訪問用戶數,對應的用戶id是11,15,19。
127.0.0.1:6379> bitcount unique:users:2016-04-05 1 3 (integer) 34.Bitmaps間的運算
bitop op destkey key[key....]bitop是一個復合操作,它可以做多個Bitmaps的and(交集)、or(并集)、not(非)、xor(異或)操作并將結果保存在destkey中。假設2016-04-04訪問網站的userid=1,2,5,9,如圖所示。
2016-04-04訪問網站的用戶Bitmaps
下面操作計算出2016-04-04和2016-04-03兩天都訪問過網站的用戶數量,如圖所示。
127.0.0.1:6379> bitop and unique:users:and:2016-04-04_03 unique: users:2016-04-03 unique:users:2016-04-03 (integer) 2 127.0.0.1:6379> bitcount unique:users:and:2016-04-04_03 (integer) 2如果想算出2016-04-04和2016-04-03任意一天都訪問過網站的用戶數量(例如月活躍就是類似這種),可以使用or求并集,具體命令如下:
127.0.0.1:6379> bitop or unique:users:or:2016-04-04_03 unique: users:2016-04-03 unique:users:2016-04-03 (integer) 2 127.0.0.1:6379> bitcount unique:users:or:2016-04-04_03 (integer) 6
利用bitop and命令計算兩天都訪問網站的用戶
3.3 Bitmaps分析
假設網站有1億用戶,每天獨立訪問的用戶有5千萬,如果每天用集合類型和Bitmaps分別存儲活躍用戶可以得到下表:
set和Bitmaps存儲一天活躍用戶的對比
很明顯,這種情況下使用Bitmaps能節省很多的內存空間。但Bitmaps并不是萬金油,假如該網站每天的獨立訪問用戶很少,例如只有10萬(大量的僵尸用戶),那么兩者的對比如下表所示,很顯然,這時候使用Bitmaps就不太合適了,因為基本上大部分位都是0。
set和Bitmaps存儲一天活躍用戶的對比(獨立用戶比較少)
4 發布訂閱
4.1 基本概念
Redis提供了基于“發布/訂閱”模式的消息機制,此種模式下,消息發布者和訂閱者不進行直接通信,發布者客戶端向指定的頻道(channel)發布消息,訂閱該頻道的每個客戶端都可以收到該消息,如圖所示。Redis提供了若干命令支持該功能,在實際應用開發時,能夠為此類問題提供實現方法。
Redis發布訂閱模型
4.2 命令
Redis主要提供了發布消息、訂閱頻道、取消訂閱以及按照模式訂閱和取消訂閱等命令。
1.發布消息
publish channel message下面操作會向channel:sports頻道發布一條消息“Tim won the championship”,返回結果為訂閱者個數,因為此時沒有訂閱,所以返回結果為0:
127.0.0.1:6379> publish channel:sports "Tim won the championship" (integer) 02.訂閱消息
subscribe channel [channel ...]訂閱者可以訂閱一個或多個頻道,下面操作為當前客戶端訂閱了 channel:sports頻道:
127.0.0.1:6379> subscribe channel:sports Reading messages... (press Ctrl-C to quit) 1) "subscribe" 2) "channel:sports" 3) (integer) 1此時另一個客戶端發布一條消息:
127.0.0.1:6379> publish channel:sports "James lost the championship" (integer) 1當前訂閱者客戶端會收到如下消息:
127.0.0.1:6379> subscribe channel:sports Reading messages... (press Ctrl-C to quit) ... 1) "message" 2) "channel:sports" 3) "James lost the championship"如果有多個客戶端同時訂閱了channel:sports,整個過程如圖3-17所示。有關訂閱命令有兩點需要注意:
·客戶端在執行訂閱命令之后進入了訂閱狀態,只能接收subscribe、psubscribe、unsubscribe、punsubscribe的四個命令。
·新開啟的訂閱客戶端,無法收到該頻道之前的消息,因為Redis不會對發布的消息進行持久化。
多個客戶端同時訂閱頻道channel:sports
開發提示
和很多專業的消息隊列系統(例如Kafka、RocketMQ)相比,Redis的發布訂閱略顯粗糙,例如無法實現消息堆積和回溯。但勝在足夠簡單,如果當前場景可以容忍的這些缺點,也不失為一個不錯的選擇。
3.取消訂閱
unsubscribe [channel [channel ...]]客戶端可以通過unsubscribe命令取消對指定頻道的訂閱,取消成功后,不會再收到該頻道的發布消息:
127.0.0.1:6379> unsubscribe channel:sports 1) "unsubscribe" 2) "channel:sports" 3) (integer) 04.3 使用場景
聊天室、公告牌、服務之間利用消息解耦都可以使用發布訂閱模式,下面以簡單的服務解耦進行說明。如圖所示,圖中有兩套業務,上面為視頻管理系統,負責管理視頻信息;下面為視頻服務面向客戶,用戶可以通過各種客戶端(手機、瀏覽器、接口)獲取到視頻信息。
發布訂閱用于視頻信息變化通知
假如視頻管理員在視頻管理系統中對視頻信息進行了變更,希望及時通知給視頻服務端,就可以采用發布訂閱的模式,發布視頻信息變化的消息到指定頻道,視頻服務訂閱這個頻道及時更新視頻信息,通過這種方式可以有效解決兩個業務的耦合性。
·視頻服務訂閱video:changes頻道如下:
·視頻管理系統發布消息到video:changes頻道如下:
publish video:changes "video1,video3,video5"·當視頻服務收到消息,對視頻信息進行更新,如下所示:
for video in video1,video3,video5 update {video}5 客戶端通信協議
幾乎所有的主流編程語言都有Redis的客戶端, 不考慮Redis非常流行的原因,如果站在技術的角度看原因還有兩個:
第一,客戶端與服務端之間的通信協議是在TCP協議之上構建的。
第二,Redis制定了RESP(REdis Serialization Protocol,Redis序列化協議)實現客戶端與服務端的正常交互,這種協議簡單高效,既能夠被機器解析,又容易被人類識別。例如客戶端發送一條set hello world命令給服務端,按照RESP的標準,客戶端需要將其封裝為如下格式(每行用\r\n分隔):
這樣Redis服務端能夠按照RESP將其解析為set hello world命令,執行后回復的格式如下:
+OK可以看到除了命令(set hello world)和返回結果(OK)本身還包含了一些特殊字符以及數字,下面將對這些格式進行說明。
1.發送命令格式
RESP的規定一條命令的格式如下,CRLF代表"\r\n"。
依然以set hell world這條命令進行說明。 參數數量為3個,因此第一行為:
*3參數字節數分別是355,因此后面幾行為:
$3 SET $5 hello $5 world有一點要注意的是,上面只是格式化顯示的結果,實際傳輸格式為如下代碼,整個過程如圖所示:
*3\r\n$3\r\nSET\r\n$5\r\nhello\r\n$5\r\nworld\r\n2.返回結果格式
Redis的返回結果類型分為以下五種,如下圖所示:
·狀態回復:在RESP中第一個字節為"+“。
·錯誤回復:在RESP中第一個字節為”-“。
·整數回復:在RESP中第一個字節為”:“。
·字符串回復:在RESP中第一個字節為”$“。
·多條字符串回復:在RESP中第一個字節為”*"。
客戶端和服務端使用RESP標準進行數據交互
Redis五種回復類型在RESP下的編碼
6 Java客戶端Jedis
Java有很多優秀的Redis客戶端(詳見:http://redis.io/clients#java),這里介紹使用較為廣泛的客戶端Jedis。
6.1 Jedis的基本使用方法
Jedis的使用方法非常簡單,只要下面三行代碼就可以實現get功能:
# 1. 生成一個Jedis對象,這個對象負責和指定Redis實例進行通信 Jedis jedis = new Jedis("127.0.0.1", 6379); # 2. jedis執行set操作 jedis.set("hello", "world"); # 3. jedis執行get操作, value="world" String value = jedis.get("hello");可以看到初始化Jedis需要兩個參數:Redis實例的IP和端口,除了這兩個參數外,還有一個包含了四個參數的構造函數是比較常用的:
Jedis(final String host, final int port, final int connectionTimeout, final int soTimeout)參數說明:
·host:Redis實例的所在機器的IP。
·port:Redis實例的端口。
·connectionTimeout:客戶端連接超時。
·soTimeout:客戶端讀寫超時。
如果想看一下執行結果:
String setResult = jedis.set("hello", "world"); String getResult = jedis.get("hello"); System.out.println(setResult); System.out.println(getResult);輸出結果為:
OK world可以看到jedis.set的返回結果是OK,和redis-cli的執行效果是一樣的,只不過結果類型變為了Java的數據類型。上面的這種寫法只是為了演示使用,在實際項目中比較推薦使用try catch finally的形式來進行代碼的書寫:一方面可以在Jedis出現異常的時候(本身是網絡操作),將異常進行捕獲或者拋出;另一個方面無論執行成功或者失敗,將Jedis連接關閉掉,在開發中關閉不用的連接資源是一種好的習慣,代碼類似如下:
Jedis jedis = null; try {jedis = new Jedis("127.0.0.1", 6379); jedis.get("hello"); } catch (Exception e) { logger.error(e.getMessage(),e); } finally { if (jedis != null) { jedis.close(); } }下面用一個例子說明Jedis對于Redis五種數據結構的操作,為了節省篇幅,所有返回結果放在注釋中。
// 1.string // 輸出結果:OK jedis.set("hello", "world"); // 輸出結果:world jedis.get("hello"); // 輸出結果:1 jedis.incr("counter"); // 2.hash jedis.hset("myhash", "f1", "v1"); jedis.hset("myhash", "f2", "v2"); // 輸出結果:{f1=v1, f2=v2} jedis.hgetAll("myhash"); // 3.list jedis.rpush("mylist", "1"); jedis.rpush("mylist", "2"); jedis.rpush("mylist", "3"); // 輸出結果:[1, 2, 3] jedis.lrange("mylist", 0, -1); // 4.set jedis.sadd("myset", "a"); jedis.sadd("myset", "b"); jedis.sadd("myset", "a"); // 輸出結果:[b, a] jedis.smembers("myset"); // 5.zset jedis.zadd("myzset", 99, "tom"); jedis.zadd("myzset", 66, "peter"); jedis.zadd("myzset", 33, "james"); // 輸出結果:[[["james"],33.0], [["peter"],66.0], [["tom"],99.0]] jedis.zrangeWithScores("myzset", 0, -1);參數除了可以是字符串,Jedis還提供了字節數組的參數,例如:
public String set(final String key, String value) public String set(final byte[] key, final byte[] value) public byte[] get(final byte[] key) public String get(final String key)有了這些API的支持,就可以將Java對象序列化為二進制,當應用需要獲取Java對象時,使用get(final byte[]key)函數將字節數組取出,然后反序列化為Java對象即可。和很多NoSQL數據庫(例如Memcache、Ehcache)的客戶端不同,Jedis本身沒有提供序列化的工具,也就是說開發者需要自己引入序列化的工具。序列化的工具有很多,例如XML、Json、谷歌的Protobuf、Facebook的Thrift等等,對于序列化工具的選擇開發者可以根據自身需求決定。
6.2 Jedis連接池的使用方法
之前介紹的是Jedis的直連方式,所謂直連是指Jedis每次都會新建TCP連接,使用后再斷開連接,對于頻繁訪問Redis的場景顯然不是高效的使用方式,如圖所示。
Jedis直連Redis
因此生產環境中一般使用連接池的方式對Jedis連接進行管理,如圖所示,所有Jedis對象預先放在池子中(JedisPool),每次要連接Redis,只需要在池子中借,用完了在歸還給池子。
Jedis連接池使用方式
客戶端連接Redis使用的是TCP協議,直連的方式每次需要建立TCP連接,而連接池的方式是可以預先初始化好Jedis連接,所以每次只需要從Jedis連接池借用即可,而借用和歸還操作是在本地進行的,只有少量的并發同步開銷,遠遠小于新建TCP連接的開銷。另外直連的方式無法限制Jedis對象的個數,在極端情況下可能會造成連接泄露,而連接池的形式可以有效的保護和控制資源的使用。但是直連的方式也并不是一無是處,下表給出兩種方式各自的優劣勢。
Jedis直連方式和連接池方式對比
Jedis提供了JedisPool這個類作為對Jedis的連接池,同時使用了Apache的通用對象池工具common-pool作為資源的管理工具,下面是使用JedisPool操作Redis的代碼示例:
1)Jedis連接池(通常JedisPool是單例的):
2)獲取Jedis對象不再是直接生成一個Jedis對象進行直連,而是從連接池直接獲取,代碼如下:
Jedis jedis = null; try {// 1. 從連接池獲取jedis對象 jedis = jedisPool.getResource(); // 2. 執行操作 jedis.get("hello"); } catch (Exception e) { logger.error(e.getMessage(),e); } finally { if (jedis != null) { // 如果使用JedisPool,close操作不是關閉連接,代表歸還連接池 jedis.close(); } }這里可以看到在finally中依然是jedis.close()操作,為什么會把連接關閉呢,這不和連接池的原則違背了嗎?但實際上Jedis的close()實現方式如下:
public void close() { // 使用Jedis連接池 if (dataSource != null) { if (client.isBroken()) { this.dataSource.returnBrokenResource(this); } else { this.dataSource.returnResource(this); } // 直連 } else { client.close(); } }參數說明:
·dataSource!=null代表使用的是連接池,所以jedis.close()代表歸還連接給連接池,而且Jedis會判斷當前連接是否已經斷開。
·dataSource=null代表直連,jedis.close()代表關閉連接。
前面GenericObjectPoolConfig使用的是默認配置,實際它提供有很多參數,例如池子中最大連接數、最大空閑連接數、最小空閑連接數、連接活性檢測,等等,例如下面代碼:
上面幾個是GenericObjectPoolConfig幾個比較常用的屬性,下表給出了Generic-ObjectPoolConfig其他屬性及其含義解釋。
GenericObjectPoolConfig的重要屬性
7 客戶端API
7.1 client list
client list命令能列出與Redis服務端相連的所有客戶端連接信息,例如下面代碼是在一個Redis實例上執行client list的結果:
127.0.0.1:6379> client list id=254487 addr=10.2.xx.234:60240 fd=1311 name= age=8888581 idle=8888581 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get id=300210 addr=10.2.xx.215:61972 fd=3342 name= age=8054103 idle=8054103 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get id=5448879 addr=10.16.xx.105:51157 fd=233 name= age=411281 idle=331077 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=ttl id=2232080 addr=10.16.xx.55:32886 fd=946 name= age=603382 idle=331060 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get id=7125108 addr=10.10.xx.103:33403 fd=139 name= age=241 idle=1 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=del id=7125109 addr=10.10.xx.101:58658 fd=140 name= age=241 idle=1 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=del ...輸出結果的每一行代表一個客戶端的信息,可以看到每行包含了十幾個屬性,它們是每個客戶端的一些執行狀態,理解這些屬性對于Redis的開發和運維人員非常有幫助。下面將選擇幾個重要的屬性進行說明,其余通過表格的形式進行展示。
(1)標識:id、addr、fd、name
這四個屬性屬于客戶端的標識:
·id:客戶端連接的唯一標識,這個id是隨著Redis的連接自增的,重啟Redis后會重置為0。
·addr:客戶端連接的ip和端口。
·fd:socket的文件描述符,與lsof命令結果中的fd是同一個,如果fd=-1 代表當前客戶端不是外部客戶端,而是Redis內部的偽裝客戶端。
·name:客戶端的名字,后面的client setName和client getName兩個命令會對其進行說明。
(2)輸入緩沖區:qbuf、qbuf-free
Redis為每個客戶端分配了輸入緩沖區,它的作用是將客戶端發送的命令臨時保存,同時Redis從會輸入緩沖區拉取命令并執行,輸入緩沖區為客戶端發送命令到Redis執行命令提供了緩沖功能,如圖所示。
client list中qbuf和qbuf-free分別代表這個緩沖區的總容量和剩余容量,Redis沒有提供相應的配置來規定每個緩沖區的大小,輸入緩沖區會根據輸入內容大小的不同動態調整,只是要求每個客戶端緩沖區的大小不能超過1G,超過后客戶端將被關閉。下面是Redis源碼中對于輸入緩沖區的硬編碼:
輸入緩沖區基本模型
輸入緩沖使用不當會產生兩個問題:
·一旦某個客戶端的輸入緩沖區超過1G,客戶端將會被關閉。
·輸入緩沖區不受maxmemory控制,假設一個Redis實例設置了 maxmemory為4G,已經存儲了2G數據,但是如果此時輸入緩沖區使用了3G,已經超過maxmemory限制,可能會產生數據丟失、鍵值淘汰、OOM等情況(如圖所示)。
輸入緩沖區超過了maxmemory
執行效果如下:
127.0.0.1:6390> info memory # Memory used_memory_human:5.00G ... maxmemory_human:4.00G ....上面已經看到,輸入緩沖區使用不當造成的危害非常大,那么造成輸入緩沖區過大的原因有哪些?輸入緩沖區過大主要是因為Redis的處理速度跟不上輸入緩沖區的輸入速度,并且每次進入輸入緩沖區的命令包含了大量bigkey,從而造成了輸入緩沖區過大的情況。還有一種情況就是Redis發生了阻塞,短期內不能處理命令,造成客戶端輸入的命令積壓在了輸入緩沖區, 造成了輸入緩沖區過大。那么如何快速發現和監控呢?監控輸入緩沖區異常的方法有兩種:
·通過定期執行client list命令,收集qbuf和qbuf-free找到異常的連接記錄并分析,最終找到可能出問題的客戶端。
·通過info命令的info clients模塊,找到最大的輸入緩沖區,例如下面命令中的其中client_biggest_input_buf代表最大的輸入緩沖區,例如可以設置超過10M就進行報警:
這兩種方法各有自己的優劣勢,下表對兩種方法進行了對比。
對比client list和info clients監控輸入緩沖區的優劣勢
運維提示
輸入緩沖區問題出現概率比較低,但是也要做好防范,在開發中要減少bigkey、減少Redis阻塞、合理的監控報警。
(3)輸出緩沖區:obl、oll、omem
Redis為每個客戶端分配了輸出緩沖區,它的作用是保存命令執行的結果返回給客戶端,為Redis和客戶端交互返回結果提供緩沖,如圖所示。與輸入緩沖區不同的是,輸出緩沖區的容量可以通過參數client-output-buffer-limit來進行設置,并且輸出緩沖區做得更加細致,按照客戶端的不同分為三種:普通客戶端、發布訂閱客戶端、slave客戶端,如圖所示。
客戶端輸出緩沖區模型
三種不同類型客戶端的輸出緩沖區
對應的配置規則是:
client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>·class:客戶端類型,分為三種。a)normal:普通客戶端;b) slave:slave客戶端,用于復制;c)pubsub:發布訂閱客戶端。
·hard limit:如果客戶端使用的輸出緩沖區大于,客戶端會被立即關閉。
·soft limit和soft seconds:如果客戶端使用的輸出緩沖區超過了并且持續了秒,客戶端會被立即關閉。
Redis的默認配置是:
client-output-buffer-limit normal 0 0 0 client-output-buffer-limit slave 256mb 64mb 60 client-output-buffer-limit pubsub 32mb 8mb 60和輸入緩沖區相同的是,輸出緩沖區也不會受到maxmemory的限制,如果使用不當同樣會造成maxmemory用滿產生的數據丟失、鍵值淘汰、OOM等情況。
監控輸出緩沖區的方法依然有兩種:
·通過定期執行client list命令,收集obl、oll、omem找到異常的連接記錄并分析,最終找到可能出問題的客戶端。
·通過info命令的info clients模塊,找到輸出緩沖區列表最大對象數,例如:
其中,client_longest_output_list代表輸出緩沖區列表最大對象數,這兩種統計方法的優劣勢和輸入緩沖區是一樣的,這里就不再贅述了。相比于輸入緩沖區,輸出緩沖區出現異常的概率相對會比較大,那么如何預防呢?方法如下:
·進行上述監控,設置閥值,超過閥值及時處理。
·限制普通客戶端輸出緩沖區的,把錯誤扼殺在搖籃中,例如可以進行如下設置:
·適當增大slave的輸出緩沖區的,如果master節點寫入較大,slave客戶端的輸出緩沖區可能會比較大,一旦slave客戶端連接因為輸出緩沖區溢出被kill,會造成復制重連。
·限制容易讓輸出緩沖區增大的命令,例如,高并發下的monitor命令就是一個危險的命令。
·及時監控內存,一旦發現內存抖動頻繁,可能就是輸出緩沖區過大。
(4)客戶端的存活狀態
client list中的age和idle分別代表當前客戶端已經連接的時間和最近一次的空閑時間:
例如上面這條記錄代表當期客戶端連接Redis的時間為603382秒,其中空閑了331060秒:
id=254487 addr=10.2.xx.234:60240 fd=1311 name= age=8888581 idle=8888581 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get例如上面這條記錄代表當期客戶端連接Redis的時間為8888581秒,其中空閑了8888581秒,實際上這種就屬于不太正常的情況,當age等于idle時,說明連接一直處于空閑狀態。 為了更加直觀地描述age和idle,下面用一個例子進行說明:
String key = "hello"; // 1) 生成jedis,并執行get操作 Jedis jedis = new Jedis("127.0.0.1", 6379); System.out.println(jedis.get(key)); // 2) 休息10秒 TimeUnit.SECONDS.sleep(10); // 3) 執行新的操作ping System.out.println(jedis.ping()); // 4) 休息5秒 TimeUnit.SECONDS.sleep(5); // 5) 關閉jedis連接 jedis.close();下面對代碼中的每一步進行分析,用client list命令來觀察age和idle參數的相應變化。
注意
為了與redis-cli的客戶端區分,本次測試客戶端IP地址:10.7.40.98。
1)在執行代碼之前,client list只有一個客戶端,也就是當前的redis-cli,下面為了節省篇幅忽略掉這個客戶端。
127.0.0.1:6379> client list id=45 addr=127.0.0.1:55171 fd=6 name= age=2 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client2)使用Jedis生成了一個新的連接,并執行get操作,可以看到IP地址為10.7.40.98的客戶端,最后執行的命令是get,age和idle分別是1秒和0秒:
127.0.0.1:6379> client list id=46 addr=10.7.40.98:62908 fd=7 name= age=1 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get3)休息10秒,此時Jedis客戶端并沒有關閉,所以age和idle一直在遞增:
127.0.0.1:6379> client list id=46 addr=10.7.40.98:62908 fd=7 name= age=9 idle=9 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get4)執行新的操作ping,發現執行后age依然在增加,而idle從0計算,也就是不再閑置:
127.0.0.1:6379> client list id=46 addr=10.7.40.98:62908 fd=7 name= age=11 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=ping5)休息5秒,觀察age和idle增加:
127.0.0.1:6379> client list id=46 addr=10.7.40.98:62908 fd=7 name= age=15 idle=5 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=ping6)關閉Jedis,Jedis連接已經消失:
redis-cli client list | grep "10.7.40.98”為空(5)客戶端的限制maxclients和timeout
Redis提供了maxclients參數來限制最大客戶端連接數,一旦連接數超過maxclients,新的連接將被拒絕。maxclients默認值是10000,可以通過info clients來查詢當前Redis的連接數:
可以通過config set maxclients對最大客戶端連接數進行動態設置:
127.0.0.1:6379> config get maxclients 1) "maxclients" 2) "10000" 127.0.0.1:6379> config set maxclients 50 OK 127.0.0.1:6379> config get maxclients 1) "maxclients" 2) "50"一般來說maxclients=10000在大部分場景下已經絕對夠用,但是某些情況由于業務方使用不當(例如沒有主動關閉連接)可能存在大量idle連接, 無論是從網絡連接的成本還是超過maxclients的后果來說都不是什么好事,因此Redis提供了timeout(單位為秒)參數來限制連接的最大空閑時間,一旦客戶端連接的idle時間超過了timeout,連接將會被關閉,例如設置timeout為30秒:
#Redis默認的timeout是0,也就是不會檢測客戶端的空閑 127.0.0.1:6379> config set timeout 30 OK下面繼續使用Jedis進行模擬,整個代碼和上面是一樣的,只不過第2)步驟休息了31秒:
String key = "hello"; // 1) 生成jedis,并執行get操作 Jedis jedis = new Jedis("127.0.0.1", 6379); System.out.println(jedis.get(key)); // 2) 休息31秒 TimeUnit.SECONDS.sleep(31); // 3) 執行get操作 System.out.println(jedis.get(key)); // 4) 休息5秒 TimeUnit.SECONDS.sleep(5); // 5) 關閉jedis連接 jedis.close();執行上述代碼可以發現在執行完第2)步之后,client list中已經沒有了Jedis的連接,也就是說timeout已經生效,將超過30秒空閑的連接關閉掉:
127.0.0.1:6379> client list id=16 addr=10.7.40.98:63892 fd=6 name= age=19 idle=19 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get # 超過timeout后,Jedis連接被關閉 redis-cli client list | grep “10.7.40.98”為空同時可以看到,在Jedis代碼中的第3)步拋出了異常,因為此時客戶端已經被關閉,所以拋出的異常是JedisConnectionException,并且提示Unexpected end of stream:
stream: world Exception in thread "main" redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream.如果將Redis的loglevel設置成debug級別,可以看到如下日志,也就是客戶端被Redis關閉的日志:
12885:M 26 Aug 08:46:40.085 - Closing idle clientRedis的默認配置給出的timeout=0,在這種情況下客戶端基本不會出現上面的異常,這是基于對客戶端開發的一種保護。例如很多開發人員在使用JedisPool時不會對連接池對象做空閑檢測和驗證,如果設置了timeout>0,可能就會出現上面的異常,對應用業務造成一定影響,但是如果Redis的客戶端使用不當或者客戶端本身的一些問題,造成沒有及時釋放客戶端連接,可能會造成大量的idle連接占據著很多連接資源,一旦超過maxclients;后果也是不堪設想。所在在實際開發和運維中,需要將timeout設置成大于0,例如可以設置為300秒,同時在客戶端使用上添加空閑檢測和驗證等等措施,例如JedisPool使用common-pool提供的三個屬性:minEvictableIdleTimeMillis、
testWhileIdle、timeBetweenEvictionRunsMillis。
(6)客戶端類型
client list中的flag是用于標識當前客戶端的類型,例如flag=S代表當前客戶端是slave客戶端、flag=N代表當前是普通客戶端,flag=O代表當前客戶端正在執行monitor命令,下表列出了11種客戶端類型。
(7)其他
上面已經將client list中重要的屬性進行了說明,下表列出之前介紹過以及一些比較簡單或者不太重要的屬性。
client list命令結果的全部屬性
7.2 monitor
monitor命令用于監控Redis正在執行的命令,如圖4-11所示,我們打開了兩個redis-cli,一個執行set get ping命令,另一個執行monitor命令。可以看到monitor命令能夠監聽其他客戶端正在執行的命令,并記錄了詳細的時間戳。
monitor命令演示
monitor的作用很明顯,如果開發和運維人員想監聽Redis正在執行的命令,就可以用monitor命令,但事實并非如此美好,每個客戶端都有自己的輸出緩沖區,既然monitor能監聽到所有的命令,一旦Redis的并發量過大,monitor客戶端的輸出緩沖會暴漲,可能瞬間會占用大量內存,下圖展示了monitor命令造成大量內存使用。
高并發下monitor命令使用大量輸出緩沖區
7.3 客戶端相關配置
·timeout:檢測客戶端空閑連接的超時時間,一旦idle時間達到了timeout,客戶端將會被關閉,如果設置為0就不進行檢測。
·maxclients:客戶端最大連接數,前面已進行分析,這里不再贅述,但是這個參數會受到操作系統設置的限制。
·tcp-keepalive:檢測TCP連接活性的周期,默認值為0,也就是不進行檢測,如果需要設置,建議為60,那么Redis會每隔60秒對它創建的TCP連接進行活性檢測,防止大量死連接占用系統資源。
·tcp-backlog:TCP三次握手后,會將接受的連接放入隊列中,tcp-
backlog就是隊列的大小,它在Redis中的默認值是511。通常來講這個參數不需要調整,但是這個參數會受到操作系統的影響,例如在Linux操作系統中,如果/proc/sys/net/core/somaxconn小于tcp-backlog,那么在Redis啟動時會看到如下日志,并建議將/proc/sys/net/core/somaxconn設置更大。
修改方法也非常簡單,只需要執行如下命令:
echo 511 > /proc/sys/net/core/somaxconn總結
以上是生活随笔為你收集整理的《Redis开发与运维》- 核心知识整理二(Lua脚本、发布订阅、客户端等)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: DevExpress.Utils.Too
- 下一篇: Microsoft CRM 3.0 Mo