1. Redis的客戶端

        共 22259字,需瀏覽 45分鐘

         ·

        2021-04-27 17:32

        最近在看《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> 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é)果,起到一個緩沖的功能。

        Redis輸入緩沖區(qū)和輸出緩沖區(qū)

        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)生大量連接超時,兩個時間點基本一致。

        慢查詢記錄
        客戶端耗時統(tǒng)計

        所以可以斷定,這個問題是慢查詢操作造成的。查看代碼,發(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ā)出報警。


        瀏覽 71
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
          
          

            1. 双腿大开蹂躏合不拢 | 国产全肉乱妇杂乱视频在线观看 | 大香蕉色色网 | 日本精品黄色视频 | 老牛AV国产性久久 |