日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > 数据库 >内容正文

数据库

《Redis开发与运维》- 核心知识整理二(Lua脚本、发布订阅、客户端等)

發布時間:2023/12/10 数据库 22 豆豆
生活随笔 收集整理的這篇文章主要介紹了 《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 = 0

3)遍歷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' 0

Redis提供了一個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服務??梢奓ua腳本雖然好用,但是使用不當破壞性也是難以想象的。

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) 0

3.獲取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) 3

4.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) 0

2.訂閱消息

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) 0

4.3 使用場景

聊天室、公告牌、服務之間利用消息解耦都可以使用發布訂閱模式,下面以簡單的服務解耦進行說明。如圖所示,圖中有兩套業務,上面為視頻管理系統,負責管理視頻信息;下面為視頻服務面向客戶,用戶可以通過各種客戶端(手機、瀏覽器、接口)獲取到視頻信息。

發布訂閱用于視頻信息變化通知

假如視頻管理員在視頻管理系統中對視頻信息進行了變更,希望及時通知給視頻服務端,就可以采用發布訂閱的模式,發布視頻信息變化的消息到指定頻道,視頻服務訂閱這個頻道及時更新視頻信息,通過這種方式可以有效解決兩個業務的耦合性。
·視頻服務訂閱video:changes頻道如下:

subscribe 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分隔):

*3 $3 SET $5 hello $5 world

這樣Redis服務端能夠按照RESP將其解析為set hello world命令,執行后回復的格式如下:

+OK

可以看到除了命令(set hello world)和返回結果(OK)本身還包含了一些特殊字符以及數字,下面將對這些格式進行說明。

1.發送命令格式
RESP的規定一條命令的格式如下,CRLF代表"\r\n"。

*<參數數量> CRLF $<參數1的字節數量> CRLF <參數1> CRLF ... $<參數N的字節數量> CRLF <參數N> CRLF

依然以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\n

2.返回結果格式
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是單例的):

// common-pool連接池配置,這里使用默認配置,后面小節會介紹具體配置說明 GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig(); // 初始化Jedis連接池 JedisPool jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379);

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 poolConfig = new GenericObjectPoolConfig(); // 設置最大連接數為默認值的5倍 poolConfig.setMaxTotal(GenericObjectPoolConfig.DEFAULT_MAX_TOTAL * 5); // 設置最大空閑連接數為默認值的3倍 poolConfig.setMaxIdle(GenericObjectPoolConfig.DEFAULT_MAX_IDLE * 3); // 設置最小空閑連接數為默認值的2倍 poolConfig.setMinIdle(GenericObjectPoolConfig.DEFAULT_MIN_IDLE * 2); // 設置開啟jmx功能 poolConfig.setJmxEnabled(true); // 設置連接池沒有連接后客戶端的最大等待時間(單位為毫秒) poolConfig.setMaxWaitMillis(3000);

上面幾個是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源碼中對于輸入緩沖區的硬編碼:

輸入緩沖區基本模型

/* Protocol and I/O related defines */ #define REDIS_MAX_QUERYBUF_LEN (1024*1024*1024) /* 1GB max query buffer. */

輸入緩沖使用不當會產生兩個問題:
·一旦某個客戶端的輸入緩沖區超過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就進行報警:

127.0.0.1:6379> info clients # Clients connected_clients:1414 client_longest_output_list:0 client_biggest_input_buf:2097152 blocked_clients:0

這兩種方法各有自己的優劣勢,下表對兩種方法進行了對比。

對比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模塊,找到輸出緩沖區列表最大對象數,例如:

127.0.0.1:6379> info clients # Clients connected_clients:502 client_longest_output_list:4869 client_biggest_input_buf:0 blocked_clients:0

其中,client_longest_output_list代表輸出緩沖區列表最大對象數,這兩種統計方法的優劣勢和輸入緩沖區是一樣的,這里就不再贅述了。相比于輸入緩沖區,輸出緩沖區出現異常的概率相對會比較大,那么如何預防呢?方法如下:
·進行上述監控,設置閥值,超過閥值及時處理。
·限制普通客戶端輸出緩沖區的,把錯誤扼殺在搖籃中,例如可以進行如下設置:

client-output-buffer-limit normal 20mb 10mb 120

·適當增大slave的輸出緩沖區的,如果master節點寫入較大,slave客戶端的輸出緩沖區可能會比較大,一旦slave客戶端連接因為輸出緩沖區溢出被kill,會造成復制重連。
·限制容易讓輸出緩沖區增大的命令,例如,高并發下的monitor命令就是一個危險的命令。
·及時監控內存,一旦發現內存抖動頻繁,可能就是輸出緩沖區過大。

(4)客戶端的存活狀態
client list中的age和idle分別代表當前客戶端已經連接的時間和最近一次的空閑時間:

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

例如上面這條記錄代表當期客戶端連接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=client

2)使用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=get

3)休息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=get

4)執行新的操作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=ping

5)休息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=ping

6)關閉Jedis,Jedis連接已經消失:

redis-cli client list | grep "10.7.40.98”為空

(5)客戶端的限制maxclients和timeout
Redis提供了maxclients參數來限制最大客戶端連接數,一旦連接數超過maxclients,新的連接將被拒絕。maxclients默認值是10000,可以通過info clients來查詢當前Redis的連接數:

127.0.0.1:6379> info clients # Clients connected_clients:1414 ...

可以通過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 client

Redis的默認配置給出的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設置更大。

# WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/ sys/net/core/somaxconn is set to the lower value of 128.

修改方法也非常簡單,只需要執行如下命令:

echo 511 > /proc/sys/net/core/somaxconn

總結

以上是生活随笔為你收集整理的《Redis开发与运维》- 核心知识整理二(Lua脚本、发布订阅、客户端等)的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。