Redis的客戶端
最近在看《Redis開發(fā)與運維》這本書,個人認為這是一本很好的 Redis 實戰(zhàn)書籍,接下來幾天將陸續(xù)更新這本書的讀書筆記,僅供大家參考。
一、客戶端通信協(xié)議
客戶端與服務(wù)端之間的通信協(xié)議是在TCP協(xié)議之上構(gòu)建的。
Redis制定了RESP(REdis Serialization Protocol,Redis序列化協(xié)議)實現(xiàn)客戶端與服務(wù)端的正常交互。
// 客戶端發(fā)送一條set hello world命令給服務(wù)端,按照 RESP 的標準,客戶端需要將其序列化為如下格式(每行用\r\n分隔)
*3
$3
SET
$5
hello
$5
world
*3:set hello world 這個數(shù)組的長度
$3:表示下面的字符的長度是3,SET的長度是3
$5:hello的長度是5
$5:world的長度是5
// Redis服務(wù)端按照 RESP 將其反序列化為 set hello world,執(zhí)行后
回復(fù) OK
+OK
二、Java客戶端Jedis
1、Jedis 基本用法
Java 有很多優(yōu)秀的 Redis 客戶端,最常用的是 Jedis 和 Redisson,Redis 官方推薦使用 Redisson,我們這里先簡單介紹一下 Jedis,后面會詳細用一篇文章來介紹 Redission。
Jedis jedis = null;
try {
// 生成一個 Jedis 對象,這個對象負責(zé)和指定Redis實例進行通信。
jedis = new Jedis("127.0.0.1", 6379);
// jedis執(zhí)行set操作
jedis.set("hello", "world");
jedis.get("hello");
} catch (Exception e) {
logger.error(e.getMessage(),e);
} finally {
if (jedis != null) {
// 關(guān)閉 Jedis 連接
jedis.close();
}
}
在實際項目中,Jedis操作放在try catch finally里更加合理,一方面可以在Jedis出現(xiàn)異常的時候(本身是網(wǎng)絡(luò)操作),將異常進行捕獲或者拋出;另一個方面無論執(zhí)行成功或者失敗,都會將Jedis連接關(guān)閉掉,在開發(fā)中關(guān)閉不用的連接資源是一種好的習(xí)慣。
2、Jedis 提供了字節(jié)數(shù)組類型的參數(shù),這樣的話,當應(yīng)用中涉及 Java 對象的時候,可以將 Java 對象序列化為二進制進行存儲,當應(yīng)用需要獲取對象時,使用 get 函數(shù)將字節(jié)數(shù)組取出,然后反序列化為Java對象即可。
// key 和 value 都是字符串
public String set(final String key, String value)
public String get(final String key)
// key 和 value 都是字節(jié)數(shù)組
public String set(final byte[] key, final byte[] value)
public byte[] get(final byte[] key)
需要注意的是,和其他NoSQL數(shù)據(jù)庫(例如 Memcache )的客戶端不同,Jedis本身沒有提供序列化的工具,也就是說開發(fā)者需要自己引入序列化的工具。
什么是序列化和反序列化?
序列化:對象 -> 字節(jié)數(shù)組(二進制)
反序列化:字節(jié)數(shù)組(二進制) -> 對象
下面我們講解一下 Jedis(Redis的客戶端) 和 protostuff(Protobuf的客戶端) 二者配合使用實現(xiàn) Redis 的序列化操作。Protobuf是谷歌提供的一個具有高效的協(xié)議數(shù)據(jù)交換格式工具庫(類似JSON),但是 Protobuf 相比于 JSON 有更高的轉(zhuǎn)化效率,時間效率和空間效率都是 JSON 的3-5倍。
1)引入 protostuff 依賴。
<protostuff.version>1.0.11</protostuff.version>
<dependency>
<groupId>com.dyuproject.protostuff</groupId>
<artifactId>protostuff-runtime</artifactId>
<version>${protostuff.version}</version>
</dependency>
<dependency>
<groupId>com.dyuproject.protostuff</groupId>
<artifactId>protostuff-core</artifactId>
<version>${protostuff.version}</version>
</dependency>
2)定義實體類。
// 俱樂部
public class Club implements Serializable {
private int id;
private String name;// 名稱
private String info;// 描述
private Date createDate;// 創(chuàng)建日期
private int rank;// 排名
// getter setter
}
3)序列化工具類 ProtostuffSerializer 封裝了序列化和反序列化方法。
import com.dyuproject.protostuff.LinkedBuffer;
import com.dyuproject.protostuff.ProtostuffIOUtil;
import com.dyuproject.protostuff.Schema;
import com.dyuproject.protostuff.runtime.RuntimeSchema;
import java.util.concurrent.ConcurrentHashMap;
public class ProtostuffSerializer {
private Schema<Club> schema = RuntimeSchema.createFrom(Club.class);
// 序列化方法
public byte[] serialize(final Club club) {
final LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
try {
return serializeInternal(club, schema, buffer);
} catch (final Exception e) {
throw new IllegalStateException(e.getMessage(), e);
} finally {
buffer.clear();
}
}
// 反序列化方法
public Club deserialize(final byte[] bytes) {
try {
Club club = deserializeInternal(bytes, schema.newMessage(), schema);
if (club != null ) {
return club;
}
} catch (final Exception e) {
throw new IllegalStateException(e.getMessage(), e);
}
return null;
}
private <T> byte[] serializeInternal(final T source, final Schema<T> schema, final LinkedBuffer buffer) {
return ProtostuffIOUtil.toByteArray(source, schema, buffer);
}
private <T> T deserializeInternal(final byte[] bytes, final T result, final Schema<T> schema) {
ProtostuffIOUtil.mergeFrom(bytes, result, schema);
return result;
}
}
4)測試。
ProtostuffSerializer protostuffSerializer = new ProtostuffSerializer();
Jedis jedis = new Jedis("127.0.0.1", 6379);
//序列化操作
String key = "club:1";
Club club = new Club(1, "AC", "米蘭", new Date(), 1);// 定義實體對象
byte[] clubBtyes = protostuffSerializer.serialize(club);// 序列化
jedis.set(key.getBytes(), clubBtyes);
// 反序列化操作
byte[] resultBtyes = jedis.get(key.getBytes());
// [id=1, clubName=AC, clubInfo=米蘭, createDate=Tue Sep 15 09:53:18 CST // 2015, rank=1]
Club resultClub = protostuffSerializer.deserialize(resultBtyes);
3、Jedis連接池
上面我們介紹的是 Jedis 的直連方式,所謂直連是指 Jedis 每次都會新建 TCP 連接,使用后再斷開連接,對于頻繁訪問 Redis 的場景顯然不是高效的使用方式。
生產(chǎn)環(huán)境中一般使用連接池的方式對Jedis連接進行管理,所有Jedis對象預(yù)先放在池子中(JedisPool),每次要連接Redis,只需要從池子中拿來就用,用完了再歸還給池子。
直連方式和連接池方式的對比見下表:
| 優(yōu)點 | 缺點 | |
|---|---|---|
| 直連 | 簡單方便,適用于少量長期連接的場景 | 1)存在每次新建/關(guān)閉TCP連接的開銷。 2)無法限制 Jedis 對象的個數(shù),在極端情況下可能會造成連接泄露。 3)Jedis 對象線程不安全。 |
| 連接池 | 1)無需每次連接都生成 Jedis 對象,降低開銷。2)使用連接池的形式可以有效地保護和控制連接資源的使用。 | 相對于直連,使用相對麻煩,尤其在資源的管理上需要很多參數(shù)來保證,一旦規(guī)劃不合理就會出現(xiàn)問題。 |
// 使用 common-pool(Apache 的通用對象池工具) 作為資源的管理工具(連接池配置)
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
// 設(shè)置最大連接數(shù)為默認值的5倍
poolConfig.setMaxTotal(GenericObjectPoolConfig.DEFAULT_MAX_TOTAL * 5);
poolConfig.setMaxIdle(GenericObjectPoolConfig.DEFAULT_MAX_IDLE * 3);
// 設(shè)置連接池沒有連接后客戶端的最大等待時間(單位為毫秒)
poolConfig.setMaxWaitMillis(3000);
// 初始化Jedis連接池
JedisPool jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379);
Jedis jedis = null;
try {
// 1. 從連接池獲取jedis對象
jedis = jedisPool.getResource();
// 2. 執(zhí)行操作
jedis.set("hello", "world");
jedis.get("hello");
} catch (Exception e) {
logger.error(e.getMessage(),e);
} finally {
if (jedis != null) {
// 如果使用JedisPool,close操作不是關(guān)閉連接,代表歸還連接池 jedis.close();
}
}
三、管理客戶端的命令
1、client list 命令
在Redis實例上執(zhí)行 client list 命令,就可以查看與該 Redis 實例相連的所有客戶端的信息,返回的信息包括:
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的開發(fā)和運維人員非常有幫助,下面我們對一些重要的信息進行說明。
1)客戶端標識
id:客戶端唯一標識,隨著Redis的連接自增,重啟 Redis后會重置為0。
addr:客戶端的ip和端口。
name:客戶端的名字。
2)輸入緩沖區(qū)和輸出緩沖區(qū)的相關(guān)信息
a)什么是輸入緩沖區(qū)和輸出緩沖區(qū)?
Redis 為每個客戶端分配了輸入緩沖區(qū)和輸出緩沖區(qū),緩沖區(qū)的作用是,在客戶端和 Redis 之間進行通信時,用來暫存客戶端發(fā)送的命令,或者是 Redis 返回給客戶端的命令執(zhí)行結(jié)果,起到一個緩沖的功能。

b)使用 client list命令,查看輸入緩沖區(qū)的內(nèi)存使用情況。
輸入緩沖區(qū):client list 命令的返回結(jié)果 qbuf 和 qbuf-free 分別代表輸入緩沖區(qū)的總?cè)萘亢褪S嗳萘俊?/p>
輸出緩沖區(qū):實際上輸出緩沖區(qū)由兩部分組成,分別是固定緩沖區(qū)(16KB)和動態(tài)緩沖區(qū),其中固定緩沖區(qū)用于暫存比較小的執(zhí)行結(jié)果,而動態(tài)緩沖區(qū)用于暫存比較大的結(jié)果,例如大的字符串、hgetall、smembers等命令的結(jié)果。client list命令的返回結(jié)果中,obl代表固定緩沖區(qū)的長度,oll代表動態(tài)緩沖區(qū)列表的長度,omem代表輸出緩沖區(qū)已經(jīng)使用的字節(jié)數(shù)。
c)輸入緩沖區(qū)溢出。
c.1)、輸入緩沖區(qū)溢出,有什么危害?
一旦輸入緩沖區(qū)溢出,可能會產(chǎn)生數(shù)據(jù)丟失、鍵值淘汰、Redis OOM、客戶端關(guān)閉的情況。
c.2)、輸入緩沖區(qū)溢出的原因?
(1)、因為Redis的處理速度跟不上輸入緩沖區(qū)的輸入速度,并且每次進入輸入緩沖區(qū)的命令包含了大量 bigkey,從而造成了輸入緩沖區(qū)過大的情況。
(2)、Redis發(fā)生了阻塞,短期內(nèi)不能處理命令,造成客戶端輸入的命令積壓在了輸入緩沖區(qū),造成了輸入緩沖區(qū)過大。
c.3)、如何避免輸入緩沖區(qū)溢出?
(1)、輸入緩沖區(qū)的上限閾值為1GB,輸入緩沖區(qū)根據(jù)輸入內(nèi)容大小的不同動態(tài)調(diào)整,不能通過參數(shù)調(diào)整。
(2)、為了防止輸入緩沖區(qū)溢出,首先在代碼里設(shè)置客戶端輸入緩沖區(qū)大小上限閾值為1GB,其次,避免客戶端寫入bigkey。
(3)、監(jiān)控輸入緩沖區(qū),一旦超過某個值就進行報警提示。info clients 命令返回的 client_biggest_input_buf 表示當前Redis中最大的輸入緩沖區(qū),可以設(shè)置它超過10M就進行報警。
127.0.0.1:6379> info clients
client_biggest_input_buf:2097152
......
d)、輸出緩沖區(qū)溢出。
d.1)、輸出緩沖區(qū)溢出,有什么危害?
和輸入緩沖區(qū)溢出一樣,一旦輸出緩沖區(qū)溢出,也可能會產(chǎn)生數(shù)據(jù)丟失、鍵值淘汰、Redis OOM、客戶端關(guān)閉的情況。
d.2)、輸出緩沖區(qū)溢出的原因?
(1)、bigkey 操作,導(dǎo)致服務(wù)器端返回的數(shù)據(jù)太大。
(2)、在 Redis 高并發(fā)場景下,執(zhí)行了 monitor 命令。
(3)、緩沖區(qū)大小設(shè)置得不合理。
d.3)、如何避免輸出緩沖區(qū)溢出?
與輸入緩沖區(qū)不同的是,輸出緩沖區(qū)的容量可以通過參數(shù) client-outputbuffer-limit 來進行設(shè)置,當輸出緩沖區(qū)大小超過設(shè)置的值,連接就會自動斷開。
(1)、避免 bigkey 操作一次性返回大量數(shù)據(jù),可以使用 scan 命令,分批次返回小量數(shù)據(jù)。
(2)、monitor 命令主要用在調(diào)試環(huán)境中,不要在生產(chǎn)環(huán)境中使用 monitor。
monitor命令用于監(jiān)聽當前Redis正在執(zhí)行的命令,一旦Redis的并發(fā)量過大,此時 Redis 中正在執(zhí)行的命令數(shù)量巨大,此時如果執(zhí)行 monitor 命令,會將 Redis 中正在執(zhí)行的所有命令寫入輸出緩沖區(qū),導(dǎo)致客戶端的輸出緩沖區(qū)暴漲甚至溢出。
(3)、使用 client-outputbuffer-limit 設(shè)置合理的緩沖區(qū)大小上限。
// class:客戶端類型,分為三種,normal:普通客戶端,slave:slave客戶端,用于主從之間復(fù)制,pubsub:發(fā)布訂閱客戶端。
// hard limit:如果客戶端使用的輸出緩沖區(qū)大于hard limit,客戶端會被立即關(guān)閉。
// soft limit和soft seconds:如果客戶端使用的輸出緩沖區(qū)超過了soft limit并且持續(xù)了soft limit秒,客戶端會被立即關(guān)閉。
client-output-buffer-limit [class] [hard limit] [soft limit] [soft seconds]
(4)、主從復(fù)制的場景,如何避免 Redis 主節(jié)點輸出緩沖區(qū)發(fā)生溢出?
如果在主從復(fù)制的過程中,Redis 主節(jié)點的輸出緩沖區(qū)發(fā)生溢出,那么所有 slave 節(jié)點的連接都將被kill,而造成復(fù)制重連,之前同步的都白費了,效率降低。
如何避免 Redis 主節(jié)點輸出緩沖區(qū)發(fā)生溢出,有三個出發(fā)點:
使用 client-outputbuffer-limit,將 Redis 主節(jié)點的輸出緩沖區(qū)設(shè)置地大一點。
控制和主節(jié)點連接的從節(jié)點個數(shù)。
控制主節(jié)點保存的數(shù)據(jù)量大小,通??刂圃?~4GB。
3)客戶端存活狀態(tài):age、idle
age:表示當前客戶端已經(jīng)連接了多長時間,idle:表示當前客戶端已經(jīng)空閑多長時間了。
見 1 中 client list 返回信息的第一條記錄,當前客戶端連接Redis的時間為 8888581 秒,空閑了 8888581 秒,這屬于不太正常的情況,因為當 age 等于 idle 時,說明連接一直處于空閑狀態(tài),Redis能連多少客戶端是有限制的,這樣的空閑連接屬于對 Redis 資源的一種浪費。
2、info clients 命令
使用 info clients 命令可以查看當前已經(jīng)連接上 Redis 的客戶端的數(shù)量。每個客戶端都有一個輸入緩存和輸出緩存,使用 info clients 命令可以獲得所有客戶端中最大的輸入緩存和輸出緩存分別是多大。
127.0.0.1:6379> info clients
// connected_clients : 已連接客戶端的數(shù)量
connected_clients:1414
// 當前連接的客戶端當中,最長的輸出列表,即最大的輸出緩存
client_longest_output_list
// 當前連接的客戶端當中,最大的輸入緩存
client_longest_input_buf
3、monitor 命令
monitor 命令用于監(jiān)聽當前 Redis 正在執(zhí)行的命令。
127.0.0.1:6379> monitor
OK
// 1378822105.089572 是時間戳,[0 127.0.0.1:56604] 中的 0 是數(shù)據(jù)庫號碼,127... 是 IP 地址和端口,"SET" "msg" "hello world" 是被執(zhí)行的命令。
1378822105.089572 [0 127.0.0.1:56604] "SET" "msg" "hello world"
1378822109.036925 [0 127.0.0.1:56604] "SET" "number" "123"
1378822140.649496 [0 127.0.0.1:56604] "SADD" "fruits" "Apple" "Banana" "Cherry"
1378822154.117160 [0 127.0.0.1:56604] "EXPIRE" "msg" "10086"
1378822257.329412 [0 127.0.0.1:56604] "KEYS" "*"
4、client kill 命令
client kill 命令用于殺掉指定IP地址和端口的客戶端。
// ip:客戶端的IP,port:客戶端的端口號。
client kill ip:port
// 客戶端列表,127.0.0.1:55593 和 127.0.0.1:52343
127.0.0.1:6379> client list
id=49 addr=127.0.0.1:55593 fd=6 name= age=9 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
id=50 addr=127.0.0.1:52343 fd=7 name= age=4 idle=4 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
// 殺掉127.0.0.1:52343這個客戶端
127.0.0.1:6379> client kill 127.0.0.1:52343
OK
// 此時客戶端列表,只剩下了127.0.0.1:55593這個客戶端
127.0.0.1:6379> client list
id=49 addr=127.0.0.1:55593 fd=6 name= age=9 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
由于一些原因(例如設(shè)置timeout=0時產(chǎn)生的長時間空閑的客戶端),需要手動殺掉客戶端連接時,可以使用client kill命令。
四、管理客戶端的參數(shù)配置
1、客戶端相關(guān)配置參數(shù)
maxclients:最大客戶端連接數(shù),即 Redis 最多能連接多少個客戶端,一旦連接數(shù)超過 maxclients,新的連接將被拒絕。maxclients默認值是10000。
timeout:連接的最大空閑時間,客戶端與 Redis 建立連接,如果這個連接的空閑時間(idle)超過了timeout,連接就會斷開,客戶端就會被關(guān)閉。如果設(shè)置為0,那么不管連接空閑多久,都不會自動斷開。
在開發(fā)和運維過程中,建議將timeout設(shè)置成大于0,防止由于客戶端使用不當或
者客戶端本身的一些問題,造成沒有及時釋放客戶端連接,從而造成大量的空閑連
接占據(jù)著很多連接資源,一旦超過maxclients,后果將不堪設(shè)想。
tcp-keepalive:檢測 Redis 與客戶端之間 TCP 連接活性的周期,即每隔多久對 TCP 活性進行一次檢測。默認值為0,也就是不進行檢測,如果需要設(shè)置,建議設(shè)置為60,那么 Redis 會每隔60秒對它創(chuàng)建的 TCP 連接進行活性檢測,防止大量死連接占用系統(tǒng)資源。
tcp-backlog:Redis 與客戶端進行 TCP 三次握手后,會將接受的連接放入內(nèi)部的隊列中,tcpbacklog就是隊列的大小,它在Redis中的默認值是511。
2、如何獲取和修改客戶端配置參數(shù)?
使用 config get 參數(shù)名,來獲取已經(jīng)配置好的參數(shù)值,使用 config set 參數(shù)名 參數(shù)值,對客戶端參數(shù)進行動態(tài)配置。
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"
五、客戶端常見異常
在 Redis 客戶端的使用過程中,無論是客戶端使用不當還是 Redis 服務(wù)端出現(xiàn) 問題,客戶端會反應(yīng)出一些異常。下面我們分析一下 Jedis 使用過程中常見的異常情況。
1、無法從連接池獲取到連接
(1)、兩種異常情況
第一種情況:JedisPool 中的 Jedis 對象個數(shù)是有限的(默認是 8 個),如果 JedisPool 中所有的 Jedis 對象都已經(jīng)被占用,此時來了一個客戶端要從 JedisPool 中借用Jedis,就需要進行等待,等待時長為 maxWaitMillis (連接池設(shè)置參數(shù)),如果在 maxWaitMillis 時間內(nèi),該客戶端仍然無法獲取到 Jedis 對象,就會拋出如下異常:
redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
......
Caused by: java.util.NoSuchElementException: Timeout waiting for idle object at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:449)
第二種情況:連接池設(shè)置了 blockWhenExhausted=false(blockWhenExhausted,當連接池中所有連接都被占用的時候,當有客戶端想要獲取連接時是否阻塞,false:不阻塞直接報異常,ture:阻塞直到超時,默認是 true),那么客戶端發(fā)現(xiàn)連接池中沒有資源時,會立即拋出異常不進行等待。
redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
......
Caused by: java.util.NoSuchElementException: Pool exhausted at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:464)
(2)、造成連接池沒有資源的原因
a)客戶端方面的原因:
1、高并發(fā)情況下連接池設(shè)置過小,供不應(yīng)求。但是正常情況下只要比默認的最大連接數(shù)(8個)多一些即可,因 為正常情況下JedisPool以及Jedis的處理效率足夠高。
2、客戶端沒有正確使用連接池,比如沒有及時釋放連接資源。
3、客戶端存在慢查詢操作,這些慢查詢持有的 Jedis 對象歸還速度會比較慢,造成連接池中沒有連接可用。
b)服務(wù)端方面的原因:
客戶端是正常的,但是 Redis 服務(wù)端由于一些原因,造成了客戶端的命令在執(zhí)行過程中發(fā)生阻塞,導(dǎo)致連接不能及時還回去。
比如 RDB 持久化時,fork 進程做內(nèi)存快照,AOF 持久化時,AOF 文件重寫,這些操作會占用大量的CPU資源,進而拖慢客戶端命令。
2、客戶端讀寫超時
(1)、讀寫超時異常情況
在使用 Jedis 調(diào)用 Redis 時,如果出現(xiàn)了讀寫超時,會報如下異常:
// 程序報下述的錯誤信息,就表示當前發(fā)生客戶端讀寫超時異常了。
redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: Read timed out
(2)、造成超時異常的原因
1、讀寫超時時間設(shè)置得過短。
2、命令本身就比較慢。
3、客戶端與服務(wù)端之間網(wǎng)絡(luò)不正常。
4、由于某些原因,Redis自身發(fā)生阻塞。
3、客戶端連接超時
(1)、連接超時異常情況
在使用 Jedis 連接 Redis 的時候,如果出現(xiàn)連接超時,會報如下異常:
redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: connect timed out
(2)、造成連接超時的原因
1、連接超時時間設(shè)置得過短,可以通過下面代碼進行設(shè)置:
// 毫秒
jedis.getClient().setConnectionTimeout(time);
2、Redis發(fā)生阻塞,造成 tcp-backlog 已滿,造成新的連接失敗,tcp-backlog 的含義見 四.1 。
3、客戶端與服務(wù)端之間網(wǎng)絡(luò)不正常。
4、客戶端數(shù)據(jù)流異常
(1)、客戶端數(shù)據(jù)流異常
// 數(shù)據(jù)流意外結(jié)束。
redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream.
(2)、造成客戶端數(shù)據(jù)流異常的原因
1、輸出緩沖區(qū)溢出。
2、長時間閑置的連接被服務(wù)端主動斷開了。
3、不正常并發(fā)讀寫,Jedis 對象同時被多個線程并發(fā)操作,可能會出現(xiàn)上述異常。
5、Jedis調(diào)用Redis時,如果Redis正在加載持久化文件,此時客戶端也會報錯。
redis.clients.jedis.exceptions.JedisDataException: LOADING Redis is loading the dataset in memory
6、Redis使用的內(nèi)存超過了 maxmemory 限制,客戶端會報錯。
redis.clients.jedis.exceptions.JedisDataException: OOM command not allowed when used memory > 'maxmemory'.
這個異常的解決方案是,調(diào)整 maxmemory 大小并找到造成內(nèi)存增長的原因。
7、Redis 連接的客戶端數(shù)量已經(jīng)達到 maxclients 限制,此時新來的客戶端嘗試連接 Redis 會報錯。
(1)、Redis 客戶端連接數(shù)過大報異常的情況
// 已達到最大客戶端數(shù)
redis.clients.jedis.exceptions.JedisDataException: ERR max number of clients reached
此時新的客戶端連接執(zhí)行任何命令,返回結(jié)果都是如下:
127.0.0.1:6379> get hello
(error) ERR max number of clients reached
六、客戶端發(fā)生異常案例分析
1、Redis內(nèi)存陡增
1)現(xiàn)象
服務(wù)端現(xiàn)象:Redis 主節(jié)點內(nèi)存陡增,幾乎用滿 maxmemory ,而從節(jié)點內(nèi)存并沒有變化。
客戶端現(xiàn)象:客戶端產(chǎn)生了 OOM 異常,也就是說 Redis 主節(jié)點使用的內(nèi)存 已經(jīng)超過了 maxmemory 的設(shè)置,無法再寫入新的數(shù)據(jù)。
2)分析原因
首先查看是否是主從復(fù)制出現(xiàn)問題導(dǎo)致主從不一致。
// 主節(jié)點的鍵個數(shù)
127.0.0.1:6379> dbsize
(integer) 2126870
// 從節(jié)點的鍵個數(shù)
127.0.0.1:6380> dbsize
(integer) 2126870
我們可以看到主從節(jié)點鍵個數(shù)一樣,可以說明復(fù)制是正常的,主從數(shù)據(jù)基本一致。
其次排查是否是由客戶端緩沖區(qū)(輸入緩沖區(qū)和輸出緩沖區(qū))溢出,造成主節(jié)點內(nèi)存陡增的。
127.0.0.1:6379> info clients
connected_clients:1891
client_longest_output_list:225698
client_biggest_input_buf:0 blocked_clients:0
很明顯輸出緩沖區(qū)不太正常,最大的客戶端輸出緩沖區(qū)隊列已經(jīng)超過了 20 萬個對象了。
因為 client list 命令返回的 omem 表示輸出緩沖區(qū)使用的字節(jié)數(shù)(一般來說大部分客戶端的 omem 為0,因為處理速度會足夠快),所以我們通過 client list命令,可以找到輸出緩沖區(qū)溢出的那個客戶端。
redis-cli client list | grep -v "omem=0"
這樣我們就找到了輸出緩沖區(qū)溢出的客戶端,客戶端標識:id 為7,IP 和端口為10.10.xx.78:56358,通過 cmd=monitor ,可以知道輸出緩沖區(qū)溢出是由于執(zhí)行了 monitor 命令。
id=7 addr=10.10.xx.78: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=224869 omem=2129300608 events=rw cmd=monitor
3)處理方法和后期處理
當發(fā)生內(nèi)存陡增問題時,在當下為了快速恢復(fù)業(yè)務(wù),只要使用 client kill 命令殺掉這個連接,讓其他客戶端恢復(fù)正常寫數(shù)據(jù)即可。
但是更為重要的是在日后如何及時發(fā)現(xiàn)和避免這種問題的發(fā)生,基本有三點:1、禁止monitor命令,例如使用rename-command命令重置 monitor命令為一個隨機字符串。
2、使用 client-outputbuffer-limit 限制輸出緩沖區(qū)的大小,使得當輸出緩沖區(qū)大小超過設(shè)置的值時,連接就會自動斷開。
3、盡量不要使用 smembers、hgetall、lrange 等返回大量數(shù)據(jù)結(jié)果的命令,避免輸出緩沖區(qū)溢出。
4、使用專業(yè)的 Redis 運維工具,在發(fā)生內(nèi)存陡增時能夠及時接受到報警信息,快速發(fā)現(xiàn)和定位問題。
2、客戶端周期性的超時
1)現(xiàn)象
客戶端現(xiàn)象:客戶端出現(xiàn)大量超時,經(jīng)過分析發(fā)現(xiàn)超時是周期性出現(xiàn)的,這為問題的查找提供了重要依據(jù)。
Caused by: redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: connect timed out
服務(wù)端現(xiàn)象:服務(wù)端并沒有明顯的異常,只是有一些慢查詢操作。
2)分析原因
首先排查是否是網(wǎng)絡(luò)原因,經(jīng)過排查網(wǎng)絡(luò)沒有問題。
再排查是否 Redis 自身出現(xiàn)問題,經(jīng)過觀察Redis日志統(tǒng)計,并沒有發(fā)現(xiàn)異常。
最后排查是否是客戶端的問題,由于超時是周期性出現(xiàn)的,所以將發(fā)生超時的時間點和慢查詢?nèi)罩镜臅r間點對比了一下,發(fā)現(xiàn)只要慢查詢出現(xiàn),客戶端就會產(chǎn)生大量連接超時,兩個時間點基本一致。


所以可以斷定,這個問題是慢查詢操作造成的。查看代碼,發(fā)現(xiàn)代碼中有個定時任務(wù),每 5 分鐘對 user_fan_hset_sort 這個 key 執(zhí)行一次 hgetall 操作,而使用 hlen 發(fā)現(xiàn) user_fan_hset_sort 這個 key 對應(yīng)的 value 中有 200 萬個元素,執(zhí)行 hgetall 必然導(dǎo)致阻塞。
127.0.0.1:6399> hlen user_fan_hset_sort
(integer) 2883279
3)處理方法和后期處理
從開發(fā)層面,盡量避免慢查詢操作,類似 smembers、hgetall、lrange 這些 O(n) 的操作。
從運維層面,監(jiān)控慢查詢,一旦超過閥值,就發(fā)出報警。
