Redis-客戶端

陳彬_smile發表於2020-12-24

本章內容如下:
·客戶端通訊協議
·Java客戶端Jedis
·客戶端管理
·客戶端常見異常
·客戶端案例分析


1 客戶端通訊協議

一, 客戶端與服務端之間的通訊協議是在TCP協議之上構建的。
第二,Redis制定了RESP(REdis Serialization Protocol, Redis序列化協議) 實現客戶端與服務端的正常互動, 這種協議簡單高效, 既能夠被機器解析, 又容易被人類識別。
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\n

2.返回結果格式
Redis的返回結果型別分為以下五種, 如圖4-2所示:
·狀態回覆: 在RESP中第一個位元組為"+"。
·錯誤回覆: 在RESP中第一個位元組為"-"。
·整數回覆: 在RESP中第一個位元組為": "。
·字串回覆: 在RESP中第一個位元組為"$"。
·多條字串回覆: 在RESP中第一個位元組為"*"。

redis-cli.c原始碼對命令結果的解析結構如下:

static sds cliFormatReplyTTY(redisReply *r, char *prefix) {
sds out = sdsempty();
switch (r->type) {
case REDIS_REPLY_ERROR:
// 處理錯誤回覆
case REDIS_REPLY_STATUS:
// 處理狀態回覆
case REDIS_REPLY_INTEGER:
// 處理整數回覆
case REDIS_REPLY_STRING:
// 處理字串回覆
case REDIS_REPLY_NIL:
// 處理空
case REDIS_REPLY_ARRAY:
// 處理多條字串回覆
return out;
}

2 Java客戶端Jedis

獲取Jedis
·2.1Jedis的基本使用

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: 客戶端讀寫超時。

在實際專案中比較推薦使用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();
}
}

·2.2Jedis連線池使用

Jedis的直連方式, 所謂直連是指Jedis每次都會新建TCP連線, 使用後再斷開連線, 對於頻繁訪問Redis的場景顯然不是高效的使用
方式, 如圖4-3所示。


因此生產環境中一般使用連線池的方式對Jedis連線進行管理, 如圖4-4所示, 所有Jedis物件預先放在池子中(JedisPool) , 每次要連線Redis, 只需要在池子中借, 用完了在歸還給池子。

客戶端連線Redis使用的是TCP協議, 直連的方式每次需要建立TCP連線, 而連線池的方式是可以預先初始化好Jedis連線, 所以每次只需要從Jedis連線池借用即可, 而借用和歸還操作是在本地進行的, 只有少量的併發同步開銷, 遠遠小於新建TCP連線的開銷。 另外直連的方式無法限制Jedis物件的個數, 在極端情況下可能會造成連線洩露, 而連線池的形式可以有效
的保護和控制資源的使用。
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();
}
}

public void close() {
// 使用Jedis連線池
if (dataSource != null) {
if (client.isBroken()) {
this.dataSource.returnBrokenResource(this);
} else {
this.dataSource.returnResource(this);
}
// 直連
} else {
client.close();
}
}

·2.3Jedis中Pipeline使用

Jedis支援Pipeline特性, 我們知道Redis提供了mget、 mset方法, 但是並沒有提供mdel方法, 如果想實現這個功
能, 可以藉助Pipeline來模擬批量刪除, 雖然不會像mget和mset那樣是一個原子命令, 但是在絕大數場景下可以使用。

public void mdel(List<String> keys) {
Jedis jedis = new Jedis("127.0.0.1");
// 1)生成pipeline物件
Pipeline pipeline = jedis.pipelined();
// 2)pipeline執行命令, 注意此時命令並未真正執行
for (String key : keys) {
pipeline.del(key);
}/
/ 3)執行命令
pipeline.sync();
}

除了pipeline.sync() , 還可以使用pipeline.syncAndReturnAll() 將pipeline的命令進行返回, 例如下面程式碼將set和incr做了一次pipeline操作,並順序列印了兩個命令的結果:

Jedis jedis = new Jedis("127.0.0.1");
Pipeline pipeline = jedis.pipelined();
pipeline.set("hello", "world");
pipeline.incr("counter");
List<Object> resultList = pipeline.syncAndReturnAll();
for (Object object : resultList) {
System.out.println(object);
}

·2.4Jedis的Lua指令碼使用

Jedis中執行Lua指令碼和redis-cli十分類似, Jedis提供了三個重要的函式實現Lua指令碼的執行:
Object eval(String script, int keyCount, String... params)
Object evalsha(String sha1, int keyCount, String... params)
String scriptLoad(String script)

String key = "hello";
String script = "return redis.call('get',KEYS[1])";
Object result = jedis.eval(script, 1, key);
// 列印結果為world
System.out.println(result)

scriptLoad和evalsha函式要一起使用, 首先使用scriptLoad將指令碼載入到Redis中, 程式碼如下:

String scriptSha = jedis.scriptLoad(script);
Stirng key = "hello";
Object result = jedis.evalsha(scriptSha, 1, key);
// 列印結果為world
System.out.println(result);

3.客戶端管理

3.1 客戶端API
1.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

(2) 輸入緩衝區: qbuf、 qbuf-free
Redis為每個客戶端分配了輸入緩衝區, 它的作用是將客戶端傳送的命令臨時儲存, 同時Redis從會輸入緩衝區拉取命令並執行, 輸入緩衝區為客戶端傳送命令到Redis執行命令提供了緩衝功能, 如圖4-5所示。
client list中qbuf和qbuf-free分別代表這個緩衝區的總容量和剩餘容量,Redis沒有提供相應的配置來規定每個緩衝區的大小, 輸入緩衝區會根據輸入內容大小的不同動態調整, 只是要求每個客戶端緩衝區的大小不能超過1G, 超過後客戶端將被關閉。
輸入緩衝使用不當會產生兩個問題:
·一旦某個客戶端的輸入緩衝區超過1G, 客戶端將會被關閉。
·輸入緩衝區不受maxmemory控制, 假設一個Redis例項設定了maxmemory為4G, 已經儲存了2G資料, 但是如果此時輸入緩衝區使用了3G, 已經超過maxmemory限制, 可能會產生資料丟失、 鍵值淘汰、 OOM等情況(如圖4-6所示)

·通過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

(3) 輸出緩衝區: obl、 oll、 omem

Redis為每個客戶端分配了輸出緩衝區, 它的作用是儲存命令執行的結果返回給客戶端, 為Redis和客戶端互動返回結果提供緩衝
與輸入緩衝區不同的是, 輸出緩衝區的容量可以通過引數client-outputbuffer-limit來進行設定, 並且輸出緩衝區做得更加細緻, 按照客戶端的不同分為三種: 普通客戶端、 釋出訂閱客戶端、 slave客戶端, 如圖4-8所示。

client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>

·<class>: 客戶端型別, 分為三種。 a) normal: 普通客戶端; b)
slave: slave客戶端, 用於複製; c) pubsub: 釋出訂閱客戶端。
·<hard limit>: 如果客戶端使用的輸出緩衝區大於<hard limit>, 客戶端
會被立即關閉。
·<soft limit>和<soft seconds>: 如果客戶端使用的輸出緩衝區超過了<soft
limit>並且持續了<soft limit>秒, 客戶端會被立即關閉。
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等情況。
實際上輸出緩衝區由兩部分組成: 固定緩衝區(16KB) 和動態緩衝區, 其中固定緩衝區返回比較小的執行結果, 而動態緩衝區返回比較大的結果, 例如大的字串、 hgetall、 smembers命令的結果等, 通過Redis原始碼中redis.h的redisClient結構體(Redis3.2版本變為Client) 可以看到兩個緩衝區的實現細節:

typedef struct redisClient {
// 動態緩衝區列表
list *reply;
// 動態緩衝區列表的長度(物件個數)
unsigned long reply_bytes;
// 固定緩衝區已經使用的位元組數
int bufpos;
// 位元組陣列作為固定緩衝區
char buf[REDIS_REPLY_CHUNK_BYTES];
} redisClient;

client list中的obl代表固定緩衝區的長度, oll代表動態緩衝區列表的長度, omem代表使用的位元組數。 例如下面代表當前客戶端的固定緩衝區的長度為0, 動態緩衝區有4869個物件, 兩個部分共使用了133081288位元組=126M記憶體:

id=7 addr=127.0.0.1:56358 fd=6 name= age=91 idle=0 flags=O db=0 sub=0 psub=0 multi=-1
qbuf=0 qbuf-free=0 obl=0 oll=4869 omem=133081288 events=rw cmd=monitor

監控輸出緩衝區的方法依然有兩種:
·通過定期執行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

(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

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

127.0.0.1:6379> info clients
# Clients
connected_clients:1414

Redis原始碼中redis.c檔案中clientsCronHandleTimeout函式就是針對timeout引數進行檢驗的, 只不過在原始碼中timeout被賦值給了server.maxidletime:

int clientsCronHandleTimeout(redisClient *c) {
// 當前時間
time_t now = server.unixtime;
// server.maxidletime就是引數timeout
if (server.maxidletime &&
// 很多客戶端驗證, 這裡就不佔用篇幅, 最重要的驗證是下面空閒時間超過了maxidletime就會
// 被關閉掉客戶端
(now - c->lastinteraction > server.maxidletime))
{
redisLog(REDIS_VERBOSE,"Closing idle client");
// 關閉客戶端
freeClient(c);
}
}

Redis的預設配置給出的timeout=0, 在這種情況下客戶端基本不會出現上面的異常, 這是基於對客戶端開發的一種保護。
3.client kill
client kill ip:port
此命令用於殺掉指定IP地址和埠的客戶端

127.0.0.1:6379> client kill 127.0.0.1:52343
OK

3.2 客戶端相關配置
·timeout: 檢測客戶端空閒連線的超時時間, 一旦idle時間達到了timeout, 客戶端將會被關閉, 如果設定為0就不進行檢測。
·maxclients: 客戶端最大連線數
·tcp-keepalive: 檢測TCP連線活性的週期, 預設值為0, 也就是不進行檢測, 如果需要設定, 建議為60, 那麼Redis會每隔60秒對它建立的TCP連線進行活性檢測, 防止大量死連線佔用系統資源。
·tcp-backlog: TCP三次握手後, 會將接受的連線放入佇列中, tcpbacklog就是佇列的大小, 它在Redis中的預設值是511。
 

備註:文章參考《Redis開發與運維》,作者:付磊,張益軍

 

 

 

 

 

 

 

 

 

 

 

 

 

 

相關文章